Add support for custom roles and role permissions management in pgAdmin. #7310

pull/8661/head
Aditya Toshniwal 2025-04-15 11:25:31 +05:30 committed by GitHub
parent 8b4df8beb1
commit 7d8a915ee0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 1438 additions and 205 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
docs/en_US/images/roles.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

BIN
docs/en_US/images/users.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -12,7 +12,7 @@ When you authenticate with pgAdmin, the server definitions associated with that
login role are made available in the tree control.
Users Tab
*******************
*********
An administrative user can use the *Users* tab to:
* manage pgAdmin users
@ -21,7 +21,7 @@ An administrative user can use the *Users* tab to:
* deactivate user
* unlock a locked user
.. image:: images/user.png
.. image:: images/users.png
:alt: pgAdmin user management window
:align: center
@ -78,6 +78,60 @@ users, but otherwise have the same capabilities as those with the *User* role.
* Click the *Help* button (?) to access online help.
Roles Tab
*********
An administrative user can use the *Roles* tab to:
* manage pgAdmin roles
* delete roles
.. image:: images/roles.png
:alt: pgAdmin roles management window
:align: center
Use the *Search* field to specify criteria and review a list of roles
that match the specified criteria. You can enter a value that matches
the following criteria types: *Role Name* or *Description*.
To add a role, click the Add (+) button at the top left corner. It will open a
dialog where you can fill in details for the new role.
.. image:: images/add_role.png
:alt: pgAdmin roles management window add new role
:align: center
Provide information about the new pgAdmin role in the row:
* Use the *Name* field to specify a unique name for the role.
* Use the *Description* field to provide a brief description of the role.
To delete a role, click the trash icon to the left of the row and confirm deletion
in the *Delete role?* dialog. If the role is associated with any users or resources,
you may need to reassign those associations before deletion.
Roles allow administrators to group privileges and assign them to users more efficiently.
This helps in managing permissions and access control within the pgAdmin client.
* Click the *Refresh* button to get the latest roles list.
* Click the *Help* button (?) to access online help.
Permissions Tab
***************
An administrative user can use the *Permissions* tab to manage pgAdmin permissions for
a role.
.. image:: images/permissions.png
:alt: pgAdmin permissions management window
:align: center
* Filter permissions using the *Search* field by entering names that match the list.
* Administrators can select permissions from the list of available permissions, and
choose to grant or revoke these permissions for specific roles.
* The permissions are applied to the selected role immediately.
Using 'setup.py' command line script
####################################
@ -108,10 +162,11 @@ email and password. role and active will be optional fields.
/path/to/python /path/to/setup.py add-user user1@gmail.com password
# to specify a role, admin and non-admin users:
# to specify a role, either you can use --admin for Administrator role or provide the
# role using --role. If both are provided --admin will be used:
/path/to/python /path/to/setup.py add-user user1@gmail.com password --admin
/path/to/python /path/to/setup.py add-user user1@gmail.com password --nonadmin
/path/to/python /path/to/setup.py add-user user1@gmail.com password --role Users
# to specify user's status
@ -132,10 +187,11 @@ followed by email, password and authentication source. email, role and status wi
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --email user1@gmail.com
# to specify a role, admin and non-admin user:
# to specify a role, either you can use --admin for Administrator role or provide the
# role using --role. If both are provided --admin will be used:
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --admin
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --nonadmin
/path/to/python /path/to/setup.py add-external-user ldapuser ldap --role Users
# to specify user's status
@ -152,10 +208,11 @@ email address. password, role and active are updatable fields.
/path/to/python /path/to/setup.py update-user user1@gmail.com --password new-password
# to specify a role, admin and non-admin user:
# to specify a role, either you can use --admin for Administrator role or provide the
# role using --role. If both are provided --admin will be used:
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --admin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --nonadmin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --admin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role Users
# to specify user's status
@ -172,17 +229,18 @@ followed by username and auth source. email, password, role and active are updat
# to change email address:
/path/to/python /path/to/setup.py update-external-user ldap ldapuser --email newemail@gmail.com
/path/to/python /path/to/setup.py update-external-user ldapuser --auth-source ldap --email newemail@gmail.com
# to specify a role, admin and non-admin user:
# to specify a role, either you can use --admin for Administrator role or provide the
# role using --role. If both are provided --admin will be used:
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --admin
/path/to/python /path/to/setup.py update-user user1@gmail.com password --role --nonadmin
/path/to/python /path/to/setup.py update-external-user user1@gmail.com password --role --admin
/path/to/python /path/to/setup.py update-external-user user1@gmail.com password --role --role Users
# to change user's status
/path/to/python /path/to/setup.py update-user ldap ldapuser --active
/path/to/python /path/to/setup.py update-user ldap ldapuser --inactive
/path/to/python /path/to/setup.py update-user ldapuser --auth-source ldap --active
/path/to/python /path/to/setup.py update-user ldapuser --auth-source ldap --inactive
Delete User
***********

View File

@ -30,6 +30,20 @@ def upgrade():
sa.Column('db_res_type', sa.String(length=32),
server_default=RESTRICTION_TYPE_DATABASES))
# For adding custom role permissions
op.add_column('role', sa.Column('permissions', sa.Text()))
# get metadata from current connection
meta = sa.MetaData()
# define table representation
meta.reflect(op.get_bind(), only=('role',))
role_table = sa.Table('role', meta)
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
op.execute(
role_table.update().where(role_table.c.name == 'User')
.values(permissions=",".join(AllPermissionTypes.list())))
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.

View File

@ -349,6 +349,8 @@ def create_app(app_name=None):
app.config['SECURITY_MSG_INVALID_PASSWORD'] = \
(gettext("Incorrect username or password."), "error")
app.config['SECURITY_PASSWORD_LENGTH_MIN'] = config.PASSWORD_LENGTH_MIN
app.config['SECURITY_MSG_UNAUTHORIZED'] = \
(gettext("Unauthorised access, permission denied."), "error")
# Create database connection object and mailer
db.init_app(app)

View File

@ -13,7 +13,7 @@ import pgadmin.browser.server_groups as sg
from flask import render_template, request, make_response, jsonify, \
current_app, url_for, session
from flask_babel import gettext
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
from psycopg.conninfo import make_conninfo, conninfo_to_dict
@ -24,6 +24,7 @@ from pgadmin.utils.ajax import make_json_response, bad_request, forbidden, \
from pgadmin.utils.crypto import encrypt, decrypt, pqencryptpassword
from pgadmin.utils.menu import MenuItem
from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
import config
from config import PG_DEFAULT_DRIVER
@ -1081,6 +1082,7 @@ class ServerNode(PGChildNodeView):
display_conn_string = make_conninfo(**con_info_ord)
return display_conn_string
@permissions_required(AllPermissionTypes.object_register_server)
@pga_login_required
def create(self, gid):
"""Add a server node to the settings database"""

View File

@ -10,6 +10,7 @@
import PGSchema from './schema.ui';
import { getNodePrivilegeRoleSchema } from '../../../../static/js/privilege.ui';
import { getNodeListByName } from '../../../../../../static/js/node_ajax';
import { AllPermissionTypes } from '../../../../../../static/js/constants';
define('pgadmin.node.schema', [
'sources/gettext', 'sources/url_for',
@ -64,7 +65,8 @@ define('pgadmin.node.schema', [
},{
name: 'generate_erd', node: 'schema', module: this,
applies: ['object', 'context'], callback: 'generate_erd',
priority: 5, label: gettext('ERD For Schema')
priority: 5, label: gettext('ERD For Schema'),
permission: AllPermissionTypes.TOOLS_ERD_TOOL,
}]);
},
can_create_schema: function(node) {

View File

@ -9,6 +9,7 @@
import { getNodeTableSchema } from './table.ui';
import _ from 'lodash';
import getApiInstance from '../../../../../../../../static/js/api_instance';
import { AllPermissionTypes } from '../../../../../../../static/js/constants';
define('pgadmin.node.table', [
'pgadmin.tables.js/enable_disable_triggers',
@ -127,7 +128,8 @@ define('pgadmin.node.table', [
priority: 5, label: gettext('ERD For Table'),
enable: (_, item) => {
return !('catalog' in pgAdmin.Browser.tree.getTreeNodeHierarchy(item));
}
},
permission: AllPermissionTypes.TOOLS_ERD_TOOL,
}
]);
pgBrowser.Events.on(

View File

@ -14,6 +14,7 @@ import DatabaseSchema from './database.ui';
import { showServerPassword } from '../../../../../../static/js/Dialogs/index';
import _ from 'lodash';
import getApiInstance, { parseApiError } from '../../../../../../static/js/api_instance';
import { AllPermissionTypes } from '../../../../../static/js/constants';
define('pgadmin.node.database', [
'sources/gettext', 'sources/url_for',
@ -122,7 +123,8 @@ define('pgadmin.node.database', [
priority: 5, label: gettext('ERD For Database'),
enable: (node) => {
return node.allowConn;
}
},
permission: AllPermissionTypes.TOOLS_ERD_TOOL,
}]);
_.bindAll(this, 'connection_lost');

View File

@ -12,6 +12,7 @@ import ServerSchema from './server.ui';
import { showServerPassword, showChangeServerPassword, showNamedRestorePoint } from '../../../../../static/js/Dialogs/index';
import _ from 'lodash';
import getApiInstance, { parseApiError } from '../../../../../static/js/api_instance';
import { AllPermissionTypes } from '../../../../static/js/constants';
define('pgadmin.node.server', [
'sources/gettext', 'sources/url_for',
@ -81,7 +82,7 @@ define('pgadmin.node.server', [
name: 'create_server_on_sg', node: 'server_group', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'register', priority: 1, label: gettext('Server...'),
data: {action: 'create'}, enable: 'canCreate',
data: {action: 'create'}, enable: 'canCreate', permission: AllPermissionTypes.OBJECT_REGISTER_SERVER
},{
name: 'disconnect_all_servers', node: 'server_group', module: this,
applies: ['object','context'], callback: 'disconnect_all_servers',
@ -91,7 +92,7 @@ define('pgadmin.node.server', [
name: 'create_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'register', priority: 3, label: gettext('Server...'),
data: {action: 'create'}, enable: 'canCreate',
data: {action: 'create'}, enable: 'canCreate', permission: AllPermissionTypes.OBJECT_REGISTER_SERVER
},{
name: 'connect_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'connect_server',
@ -167,7 +168,7 @@ define('pgadmin.node.server', [
name: 'copy_server', node: 'server', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
label: gettext('Copy Server...'), data: {action: 'copy'},
priority: 4,
priority: 4, permission: AllPermissionTypes.OBJECT_REGISTER_SERVER,
}]);
_.bindAll(this, 'connection_lost');

View File

@ -11,6 +11,7 @@ import pgAdmin from 'sources/pgadmin';
import Menu, { MenuItem } from '../../../static/js/helpers/Menu';
import getApiInstance from '../../../static/js/api_instance';
import url_for from 'sources/url_for';
import withCheckPermission from './withCheckPermission';
const MAIN_MENUS = [
{ label: gettext('File'), name: 'file', id: 'mnu_file', index: 0, addSeprator: true, hasDynamicMenuItems: false },
@ -71,7 +72,7 @@ export default class MainMenuFactory {
}
static createMenuItem(options) {
return new MenuItem({...options, callback: () => {
const callback = () => {
// Some callbacks registered in 'callbacks' check and call specifiec callback function
if (options.module && 'callbacks' in options.module && options.module.callbacks[options.callback]) {
options.module.callbacks[options.callback].apply(options.module, [options.data, pgAdmin.Browser.tree?.selected()]);
@ -89,7 +90,8 @@ export default class MainMenuFactory {
pgAdmin.Browser.notifier.error(gettext('Error in opening window'));
});
}
}}, (menu, item)=> {
};
return new MenuItem({...options, callback: withCheckPermission(options, callback)}, (menu, item)=> {
pgAdmin.Browser.Events.trigger('pgadmin:enable-disable-menu-items', menu, item);
window.electronUI?.enableDisableMenuItems(menu?.serialize(), item?.serialize());
});

View File

@ -391,6 +391,7 @@ define('pgadmin.browser', [
checked: _m.checked,
below: _m.below,
applies: _m.applies,
permission: _m.permission,
};
};

View File

@ -7,6 +7,7 @@
//
//////////////////////////////////////////////////////////////
import _ from 'lodash';
import { AllPermissionTypes } from './constants';
define([
'sources/gettext', 'sources/pgadmin',
@ -53,6 +54,7 @@ define([
name: 'show_query_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_query_tool',
priority: 998, label: gettext('Query Tool'),
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}]);
// show search objects same as query tool
@ -60,6 +62,7 @@ define([
name: 'search_objects', node: this.type, module: this,
applies: ['context'], callback: 'show_search_objects',
priority: 997, label: gettext('Search Objects...'),
permission: AllPermissionTypes.TOOLS_SEARCH_OBJECTS,
}]);
// show psql tool same as query tool.
@ -68,6 +71,7 @@ define([
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool'),
permission: AllPermissionTypes.TOOLS_PSQL_TOOL,
}]);
}
}

View File

@ -116,3 +116,21 @@ export const WEEKDAYS = [
];
export const PGAGENT_MONTHDAYS = [...MONTHDAYS].concat([{label: gettext('Last day'), value: 'Last Day'}]);
export const AllPermissionTypes = {
OBJECT_REGISTER_SERVER: 'object_register_server',
TOOLS_ERD_TOOL: 'tools_erd_tool',
TOOLS_QUERY_TOOL: 'tools_query_tool',
TOOLS_DEBUGGER: 'tools_debugger',
TOOLS_PSQL_TOOL: 'tools_psql_tool',
TOOLS_BACKUP: 'tools_backup',
TOOLS_RESTORE: 'tools_restore',
TOOLS_IMPORT_EXPORT_DATA: 'tools_import_export_data',
TOOLS_IMPORT_EXPORT_SERVERS: 'tools_import_export_servers',
TOOLS_SEARCH_OBJECTS: 'tools_search_objects',
TOOLS_MAINTENANCE: 'tools_maintenance',
TOOLS_SCHEMA_DIFF: 'tools_schema_diff',
TOOLS_GRANT_WIZARD: 'tools_grant_wizard',
STORAGE_ADD_FOLDER: 'storage_add_folder',
STORAGE_REMOVE_FOLDER: 'storage_remove_folder'
};

View File

@ -9,7 +9,7 @@
import _ from 'lodash';
import getApiInstance from '../../../static/js/api_instance';
import { BROWSER_PANELS } from './constants';
import { AllPermissionTypes, BROWSER_PANELS } from './constants';
import React from 'react';
import ObjectNodeProperties from '../../../misc/properties/ObjectNodeProperties';
import ErrorBoundary from '../../../static/js/helpers/ErrorBoundary';
@ -161,6 +161,7 @@ define('pgadmin.browser.node', [
function() {
return !!(self.canDrop(...arguments));
} : (!!self.canDrop),
permission: self.type == 'server' ? 'object_regiter_server' : undefined,
}]);
if (self.canDropCascade) {
@ -202,6 +203,7 @@ define('pgadmin.browser.node', [
priority: 998,
label: gettext('Query Tool'),
enable: enable,
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}]);
// show search objects same as query tool
@ -210,6 +212,7 @@ define('pgadmin.browser.node', [
applies: ['context'], callback: 'show_search_objects',
priority: 997, label: gettext('Search Objects...'),
icon: 'fa fa-search', enable: enable,
permission: AllPermissionTypes.TOOLS_SEARCH_OBJECTS,
}]);
if(pgAdmin['enable_psql']) {
@ -218,6 +221,7 @@ define('pgadmin.browser.node', [
name: 'show_psql_tool', node: this.type, module: this,
applies: ['context'], callback: 'show_psql_tool',
priority: 998, label: gettext('PSQL Tool'),
permission: AllPermissionTypes.TOOLS_PSQL_TOOL,
}]);
}
}
@ -247,6 +251,7 @@ define('pgadmin.browser.node', [
data_disabled: gettext('The selected tree node does not support this option.'),
},
enable: self.check_user_permission,
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}]);
});
}

View File

@ -0,0 +1,27 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import pgAdmin from 'sources/pgadmin';
import current_user from 'pgadmin.user_management.current_user';
import gettext from 'sources/gettext';
export default function withCheckPermission(options, callback) {
// Check if the user has permission to access the menu item
return ()=>{
// if the permission are not provided then no restrictions.
if(!options.permission || (options.permission && current_user.permissions?.includes(options.permission))) {
callback();
} else {
pgAdmin.Browser.notifier.alert(
gettext('Permission Denied'),
gettext('You dont have the necessary permissions to access this feature. Please contact your administrator for assistance')
);
}
};
}

View File

@ -29,6 +29,7 @@ const StyledBox = styled(Box)(({theme}) => ({
'& .SectionContainer-cardTitle': {
padding: '0.25rem 0.5rem',
fontWeight: 'bold',
width: '100%',
}
},
}));
@ -50,7 +51,7 @@ export default function SectionContainer({title, titleExtras, children, style})
}
SectionContainer.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.any.isRequired,
titleExtras: PropTypes.node,
children: PropTypes.node.isRequired,
style: PropTypes.object,

View File

@ -35,6 +35,7 @@ from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import PREF_LABEL_OPTIONS, MIMETYPE_APP_JS, \
MY_STORAGE
from pgadmin.settings.utils import get_file_type_setting
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
# Checks if platform is Windows
if _platform == "win32":
@ -345,6 +346,20 @@ class Filemanager():
return last_dir
@staticmethod
def check_capability_permission(capability):
"""
Check if the user has permission for the capability
"""
if capability == 'create':
return current_user.has_permission(
AllPermissionTypes.storage_add_folder)
elif capability == 'delete':
return current_user.has_permission(
AllPermissionTypes.storage_remove_folder)
return True
@staticmethod
def create_new_transaction(params):
"""
@ -366,16 +381,20 @@ class Filemanager():
show_volumes = isinstance(storage_dir, list) or not storage_dir
supp_types = allow_upload_files = params.get('supported_types', [])
allow_folder_create = ['create'] if \
Filemanager.check_capability_permission('create') else []
allow_folder_delete = ['delete'] if \
Filemanager.check_capability_permission('delete') else []
# tuples with (capabilities, files_only, folders_only, title)
capability_map = {
'select_file': (
['select_file', 'rename', 'upload', 'delete'],
['select_file', 'rename', 'upload'] + allow_folder_delete,
True,
False,
gettext("Select File")
),
'select_folder': (
['select_folder', 'rename', 'create'],
['select_folder', 'rename'] + allow_folder_create,
False,
True,
gettext("Select Folder")
@ -387,14 +406,14 @@ class Filemanager():
gettext("Select File")
),
'create_file': (
['select_file', 'rename', 'create'],
['select_file', 'rename'] + allow_folder_create,
True,
False,
gettext("Create File")
),
'storage_dialog': (
['select_folder', 'select_file', 'download',
'rename', 'delete', 'upload', 'create'],
['select_folder', 'select_file', 'download', 'rename',
'upload'] + allow_folder_delete + allow_folder_create,
True,
False,
gettext("Storage Manager")
@ -767,7 +786,9 @@ class Filemanager():
stored in the session
"""
trans_data = Filemanager.get_trasaction_selection(self.trans_id)
return False if capability not in trans_data['capabilities'] else True
# capability
return False if capability not in trans_data['capabilities'] \
else Filemanager.check_capability_permission(capability)
def getfolder(self, path=None, file_type="", show_hidden=False):
"""

View File

@ -873,4 +873,4 @@ FileManager.propTypes = {
onCancel: PropTypes.func,
sharedStorages: PropTypes.array,
restrictedSharedStorage: PropTypes.array,
};
};

View File

@ -17,10 +17,11 @@ import AccountTreeRoundedIcon from '@mui/icons-material/AccountTreeRounded';
import { PgIconButton } from '../../../../static/js/components/Buttons';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import { WORKSPACES } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, WORKSPACES } from '../../../../browser/static/js/constants';
import { useWorkspace } from './WorkspaceProvider';
import { LAYOUT_EVENTS } from '../../../../static/js/helpers/Layout';
import gettext from 'sources/gettext';
import withCheckPermission from '../../../../browser/static/js/withCheckPermission';
const StyledWorkspaceButton = styled(PgIconButton)(({theme}) => ({
'&.Buttons-iconButtonDefault': {
@ -44,7 +45,7 @@ const StyledWorkspaceButton = styled(PgIconButton)(({theme}) => ({
},
}));
function WorkspaceButton({menuItem, value, ...props}) {
function WorkspaceButton({menuItem, value, options, ...props}) {
const {currentWorkspace, hasOpenTabs, getLayoutObj, onWorkspaceDisabled, changeWorkspace} = useWorkspace();
const active = value == currentWorkspace;
const [disabled, setDisabled] = useState();
@ -75,12 +76,16 @@ function WorkspaceButton({menuItem, value, ...props}) {
}, [disabled]);
return (
<StyledWorkspaceButton className={active ? 'active': ''} title={menuItem?.label??''} {...props}
<StyledWorkspaceButton className={active ? 'active': ''} title={menuItem?.label??''}
{...props}
onClick={()=>{
if(menuItem) {
menuItem?.callback();
} else {
changeWorkspace(value);
// Check permission and call.
withCheckPermission(options, () => {
changeWorkspace(value);
})();
}
}}
disabled={disabled}
@ -91,7 +96,8 @@ WorkspaceButton.propTypes = {
menuItem: PropTypes.object,
active: PropTypes.bool,
changeWorkspace: PropTypes.func,
value: PropTypes.string
value: PropTypes.string,
options: PropTypes.object,
};
const Root = styled('div')(({theme}) => ({
@ -124,9 +130,9 @@ export default function WorkspaceToolbar() {
return (
<Root>
<WorkspaceButton icon={<AccountTreeRoundedIcon />} value={WORKSPACES.DEFAULT} title={gettext('Default Workspace')} tooltipPlacement="right" />
<WorkspaceButton icon={<QueryToolIcon />} value={WORKSPACES.QUERY_TOOL} title={gettext('Query Tool Workspace')} tooltipPlacement="right" />
{pgAdmin['enable_psql'] && <WorkspaceButton icon={<TerminalRoundedIcon style={{height: '1.4rem'}}/>} value={WORKSPACES.PSQL_TOOL} title={gettext('PSQL Tool Workspace')} tooltipPlacement="right" />}
<WorkspaceButton icon={<SchemaDiffIcon />} value={WORKSPACES.SCHEMA_DIFF_TOOL} title={gettext('Schema Diff Workspace')} tooltipPlacement="right" />
<WorkspaceButton icon={<QueryToolIcon />} value={WORKSPACES.QUERY_TOOL} title={gettext('Query Tool Workspace')} tooltipPlacement="right" options={{permission: AllPermissionTypes.TOOLS_QUERY_TOOL}} />
{pgAdmin['enable_psql'] && <WorkspaceButton icon={<TerminalRoundedIcon style={{height: '1.4rem'}}/>} value={WORKSPACES.PSQL_TOOL} title={gettext('PSQL Tool Workspace')} tooltipPlacement="right" options={{permission: AllPermissionTypes.TOOLS_PSQL_TOOL}} />}
<WorkspaceButton icon={<SchemaDiffIcon />} value={WORKSPACES.SCHEMA_DIFF_TOOL} title={gettext('Schema Diff Workspace')} tooltipPlacement="right" options={{permission: AllPermissionTypes.TOOLS_SCHEMA_DIFF}} />
<Box marginTop="auto">
<WorkspaceButton icon={<SettingsIcon />} menuItem={menus['settings']} title={gettext('Preferences')} tooltipPlacement="right" />
</Box>

View File

@ -59,6 +59,29 @@ roles_users = db.Table(
)
class PgAdminDbArrayString(types.TypeDecorator):
cache_ok = True
impl = types.String
def process_bind_param(self, value, dialect):
try:
if len(value) == 0:
return None
return ",".join(value)
except Exception as _:
return None
def process_result_value(self, value, dialect):
try:
if value == '':
return []
return value.split(',')
except Exception as _:
return []
class PgAdminDbBinaryString(types.TypeDecorator):
"""
To make binary string storing compatible with both
@ -92,6 +115,27 @@ class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(128), unique=True, nullable=False)
description = db.Column(db.String(256), nullable=False)
# permissions needs to be an array, use custom type to support
# both SQLite and PostgreSQL
permissions = db.Column(PgAdminDbArrayString())
def get_permissions(self):
from pgadmin.tools.user_management.PgAdminPermissions \
import AllPermissionTypes
if self.name == 'Administrator':
return AllPermissionTypes.list()
return super().get_permissions()
# We override the default UserMixin to change behaviour of has_permission
# Administrator has all permissions
class CustomUserMixin(UserMixin):
def has_permission(self, permission: str) -> bool:
if 'Administrator' in self.roles:
return True
return super().has_permission(permission)
class User(db.Model, UserMixin):

View File

@ -65,15 +65,16 @@ export default function(basicSettings) {
primary: '#d4d4d4',
muted: '#8A8A8A',
},
checkbox: {
disabled: '#6b6b6b'
},
background: {
paper: '#1e1e1e',
default: '#1e1e1e',
}
},
custom: {
checkbox: {
borderColor: '#4a4a4a',
disabled: '#6b6b6b'
},
icon: {
main: '#6b6b6b',
contrastText: '#fff',

View File

@ -63,15 +63,16 @@ export default function(basicSettings) {
primary: '#fff',
muted: '#8b9cac',
},
checkbox: {
disabled: '#6b6b6b'
},
background: {
paper: '#010B15',
default: '#010B15',
},
},
custom: {
checkbox: {
borderColor: '#A6B7C8',
disabled: '#6b6b6b'
},
icon: {
main: '#010B15',
contrastText: '#fff',

View File

@ -644,12 +644,12 @@ function getFinalTheme(baseTheme) {
styleOverrides: {
root: {
padding: '0px',
color: baseTheme.otherVars.inputBorderColor,
color: baseTheme.custom.checkbox.borderColor,
},
colorPrimary: {
'&.Mui-disabled': {
color: baseTheme.palette.checkbox.disabled
color: baseTheme.custom.checkbox.disabled
}
}
}
@ -658,12 +658,12 @@ function getFinalTheme(baseTheme) {
styleOverrides: {
root: {
padding: '0px',
color: baseTheme.otherVars.inputBorderColor,
color: baseTheme.custom.checkbox.borderColor,
},
colorPrimary: {
'&.Mui-disabled': {
color: baseTheme.palette.checkbox.disabled
color: baseTheme.custom.checkbox.disabled
}
}
}

View File

@ -63,15 +63,16 @@ export default function(basicSettings) {
primary: '#222',
muted: '#646B82',
},
checkbox: {
disabled: '#ebeef3'
},
background: {
paper: '#fff',
default: '#fff',
},
},
custom: {
checkbox: {
borderColor: '#bac1cd',
disabled: '#ebeef3'
},
icon: {
main: '#fff',
contrastText: '#222',

View File

@ -1280,7 +1280,8 @@ const StyledNotifierMessageBox = styled(Box)(({theme}) => ({
'& .FormFooter-message': {
color: theme.palette.text.primary,
marginLeft: theme.spacing(0.5),
whiteSpace: 'pre-line'
whiteSpace: 'pre-line',
userSelect: 'text'
},
'& .FormFooter-messageCenter': {
color: theme.palette.text.primary,

View File

@ -122,6 +122,7 @@ export class MenuItem {
'name', 'label', 'priority', 'module', 'callback', 'data', 'enable',
'category', 'target', 'url', 'node', 'single',
'checked', 'below', 'menu_items', 'is_checkbox', 'action', 'applies', 'is_native_only', 'type',
'permission',
];
let defaults = {
url: '#',
@ -169,16 +170,12 @@ export class MenuItem {
return this.menu_items;
}
contextMenuCallback(self) {
self.callback();
}
getContextItem(label, is_disabled, sub_ctx_item) {
let self = this;
return {
name: label,
disabled: is_disabled,
callback: () => { this.contextMenuCallback(self); },
callback: self.callback.bind(self),
...(sub_ctx_item && Object.keys(sub_ctx_item).length > 0) && { items: sub_ctx_item }
};
}

View File

@ -25,10 +25,11 @@ from config import PG_DEFAULT_DRIVER
# This unused import is required as API test cases will fail if we remove it,
# Have to identify the cause and then remove it.
from pgadmin.model import Server, SharedServer
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.misc.bgprocess import escape_dquotes_process_arg
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_NOT_FOUND
from pgadmin.tools.grant_wizard import get_data
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
# set template path for sql scripts
MODULE_NAME = 'backup'
@ -182,19 +183,6 @@ def index():
return bad_request(errormsg=gettext("This URL cannot be called directly."))
@blueprint.route("/backup.js")
@pga_login_required
def script():
"""render own javascript"""
return Response(
response=render_template(
"backup/js/backup.js", _=_
),
status=200,
mimetype=MIMETYPE_APP_JS
)
def _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
manager):
"""
@ -391,6 +379,7 @@ def _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
@blueprint.route(
'/job/<int:sid>/object', methods=['POST'], endpoint='create_object_job'
)
@permissions_required(AllPermissionTypes.tools_backup)
@pga_login_required
def create_backup_objects_job(sid):
"""

View File

@ -12,6 +12,7 @@ import BackupGlobalSchema, {getMiscellaneousSchema as getMiscellaneousGlobalSche
import getApiInstance from 'sources/api_instance';
import {retrieveAncestorOfTypeServer} from 'sources/tree/tree_utils';
import pgAdmin from 'sources/pgadmin';
import { AllPermissionTypes } from '../../../../browser/static/js/constants';
// Backup dialog
define([
@ -66,6 +67,7 @@ define([
data: {
data_disabled: gettext('Please select any server from the object explorer to take Backup of global objects.'),
},
permission: AllPermissionTypes.TOOLS_BACKUP,
}, {
name: 'backup_server',
module: this,
@ -78,6 +80,7 @@ define([
data: {
data_disabled: gettext('Please select any server from the object explorer to take Server Backup.'),
},
permission: AllPermissionTypes.TOOLS_BACKUP,
}, {
name: 'backup_global_ctx',
module: this,
@ -91,6 +94,7 @@ define([
data: {
data_disabled: gettext('Please select any database or schema or table from the object explorer to take Backup.'),
},
permission: AllPermissionTypes.TOOLS_BACKUP,
}, {
name: 'backup_server_ctx',
module: this,
@ -104,6 +108,7 @@ define([
data: {
data_disabled: gettext('Please select any server from the object explorer to take Server Backup.'),
},
permission: AllPermissionTypes.TOOLS_BACKUP,
}, {
name: 'backup_object',
module: this,
@ -118,6 +123,7 @@ define([
data: {
data_disabled: gettext('Please select any database or schema or table from the object explorer to take Backup.'),
},
permission: AllPermissionTypes.TOOLS_BACKUP,
}];
for (let node_val of menuUtils.backupSupportedNodes) {

View File

@ -16,6 +16,7 @@ import copy
from flask import render_template, request, current_app
from flask_babel import gettext
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
from werkzeug.user_agent import UserAgent
@ -34,6 +35,7 @@ from pgadmin.browser.server_groups.servers.databases.extensions.utils \
import get_extension_details
from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \
SERVER_CONNECTION_CLOSED
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
from pgadmin.preferences import preferences
MODULE_NAME = 'debugger'
@ -375,6 +377,7 @@ def check_node_type(node_type, fid, trid, conn, ppas_server,
'/init/<node_type>/<int:sid>/<int:did>/<int:scid>/<int:fid>/<int:trid>',
methods=['GET'], endpoint='init_for_trigger'
)
@permissions_required(AllPermissionTypes.tools_debugger)
@pga_login_required
def init_function(node_type, sid, did, scid, fid, trid=None):
"""

View File

@ -24,7 +24,7 @@ import FunctionArguments from './debugger_ui';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import DebuggerComponent from './components/DebuggerComponent';
import Theme from '../../../../static/js/Theme';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
@ -66,6 +66,7 @@ export default class DebuggerModule {
object: 'function',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'global_debugger',
node: 'function',
@ -80,6 +81,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'procedure_direct_debugger',
node: 'procedure',
@ -93,6 +95,7 @@ export default class DebuggerModule {
object: 'procedure',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'procedure_indirect_debugger',
node: 'procedure',
@ -107,6 +110,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'trigger_function_indirect_debugger',
node: 'trigger_function',
@ -121,6 +125,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'trigger_indirect_debugger',
node: 'trigger',
@ -135,6 +140,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'package_function_direct_debugger',
node: 'edbfunc',
@ -148,6 +154,7 @@ export default class DebuggerModule {
object: 'edbfunc',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'package_function_global_debugger',
node: 'edbfunc',
@ -162,6 +169,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'package_procedure_direct_debugger',
node: 'edbproc',
@ -175,6 +183,7 @@ export default class DebuggerModule {
object: 'edbproc',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}, {
name: 'package_procedure_global_debugger',
node: 'edbproc',
@ -189,6 +198,7 @@ export default class DebuggerModule {
debug_type: 'indirect',
},
enable: 'canDebug',
permission: AllPermissionTypes.TOOLS_DEBUGGER,
}
]);
}

View File

@ -12,6 +12,7 @@ import json
from flask import url_for, request, Response
from flask import render_template, current_app as app
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
from flask_babel import gettext
from werkzeug.user_agent import UserAgent
@ -32,6 +33,7 @@ from pgadmin.utils.constants import PREF_LABEL_KEYBOARD_SHORTCUTS, \
from .utils import ERDHelper
from pgadmin.utils.exception import ConnectionLost
from pgadmin.authenticate import socket_login_required
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
from ... import socketio
MODULE_NAME = 'erd'
@ -451,6 +453,7 @@ blueprint = ERDModule(MODULE_NAME, __name__, static_url_path='/static')
methods=["POST"],
endpoint='panel'
)
@permissions_required(AllPermissionTypes.tools_erd_tool)
@pga_login_required
def panel(trans_id):
"""

View File

@ -16,7 +16,7 @@ import ReactDOM from 'react-dom/client';
import ERDTool from './erd_tool/components/ERDTool';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
@ -59,6 +59,7 @@ export default class ERDModule {
data: {
data_disabled: gettext('The selected tree node does not support this option.'),
},
permission: AllPermissionTypes.TOOLS_ERD_TOOL,
}]);
return this;
}

View File

@ -13,6 +13,7 @@ import json
from flask import Response, url_for
from flask import render_template, request, current_app
from flask_babel import gettext
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
from urllib.parse import unquote
@ -27,6 +28,7 @@ from pgadmin.utils.ajax import precondition_required
from functools import wraps
from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
# set template path for sql scripts
MODULE_NAME = 'grant_wizard'
@ -122,19 +124,10 @@ def index():
)
@blueprint.route("/grant_wizard.js")
@pga_login_required
def script():
"""render own javascript"""
return Response(response=render_template(
"grant_wizard/js/grant_wizard.js", _=gettext),
status=200,
mimetype=MIMETYPE_APP_JS)
@blueprint.route(
'/acl/<int:sid>/<int:did>/', methods=['GET'], endpoint='acl'
)
@permissions_required(AllPermissionTypes.tools_grant_wizard)
@pga_login_required
@check_precondition
def acl_list(sid, did):

View File

@ -8,7 +8,7 @@
//////////////////////////////////////////////////////////////
import React from 'react';
import GrantWizard from './GrantWizard';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
// Grant Wizard
@ -49,6 +49,7 @@ define([
data: {
data_disabled: gettext('Please select any database, schema or schema objects from the object explorer to access Grant Wizard Tool.'),
},
permission: AllPermissionTypes.TOOLS_GRANT_WIZARD,
}];
// Add supported menus into the menus list

View File

@ -14,7 +14,7 @@ import copy
from flask import Response, render_template, request, current_app
from flask_babel import gettext as _
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, get_storage_directory, IS_WIN, \
@ -25,6 +25,7 @@ from config import PG_DEFAULT_DRIVER
from pgadmin.model import Server
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_NOT_FOUND
from pgadmin.settings import get_setting, store_setting
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
MODULE_NAME = 'import_export'
@ -227,6 +228,7 @@ def _save_import_export_settings(settings):
@blueprint.route('/job/<int:sid>', methods=['POST'], endpoint="create_job")
@permissions_required(AllPermissionTypes.tools_import_export_data)
@pga_login_required
def create_import_export_job(sid):
"""

View File

@ -10,6 +10,7 @@
import getApiInstance from 'sources/api_instance';
import ImportExportSchema from './import_export.ui';
import { getNodeListByName, getNodeAjaxOptions } from '../../../../browser/static/js/node_ajax';
import { AllPermissionTypes } from '../../../../browser/static/js/constants';
define([
'sources/gettext', 'sources/url_for',
@ -51,6 +52,7 @@ define([
data: {
data_disabled: gettext('Please select any table from the object explorer to Import/Export data.'),
},
permission: AllPermissionTypes.TOOLS_IMPORT_EXPORT_DATA,
}]);
},
getUISchema: function(treeItem) {

View File

@ -16,7 +16,7 @@ import secrets
from flask import Response, render_template, request
from flask_babel import gettext as _
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import bad_request
@ -27,7 +27,7 @@ from pgadmin.model import ServerGroup, Server
from pgadmin.utils import clear_database_servers, dump_database_servers,\
load_database_servers, validate_json_data, filename_with_file_manager_path
from urllib.parse import unquote
from pgadmin.utils.paths import get_storage_directory
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
MODULE_NAME = 'import_export_servers'
@ -60,18 +60,6 @@ def index():
return bad_request(errormsg=_("This URL cannot be called directly."))
@blueprint.route("/js/import_export_servers.js")
@pga_login_required
def script():
"""render the import/export javascript file"""
return Response(
response=render_template(
"import_export_servers/js/import_export_servers.js", _=_),
status=200,
mimetype=MIMETYPE_APP_JS
)
@blueprint.route('/get_servers', methods=['GET'], endpoint='get_servers')
@pga_login_required
def get_servers():
@ -169,6 +157,7 @@ def load_servers():
@blueprint.route('/save', methods=['POST'], endpoint='save')
@permissions_required(AllPermissionTypes.tools_import_export_servers)
@pga_login_required
def save():
"""

View File

@ -38,6 +38,7 @@ export default class ImportExportServersModule {
enable: isDefaultWorkspace,
priority: 3,
label: gettext('Import/Export Servers...'),
permission: 'tools_import_export_servers',
}];
pgAdmin.Browser.add_menus(menus);

View File

@ -13,6 +13,7 @@ import json
from flask import Response, render_template, request, current_app
from flask_babel import gettext as _
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, html, does_utility_exist, get_server
@ -22,6 +23,7 @@ from pgadmin.utils.driver import get_driver
from config import PG_DEFAULT_DRIVER
from pgadmin.model import Server, SharedServer
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_NOT_FOUND
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
MODULE_NAME = 'maintenance'
@ -129,17 +131,6 @@ def index():
)
@blueprint.route("/js/maintenance.js")
@pga_login_required
def script():
"""render the maintenance tool of vacuum javascript file"""
return Response(
response=render_template("maintenance/js/maintenance.js", _=_),
status=200,
mimetype=MIMETYPE_APP_JS
)
def get_index_name(data):
"""
Check and get index name from constraints.
@ -160,6 +151,7 @@ def get_index_name(data):
@blueprint.route(
'/job/<int:sid>/<int:did>', methods=['POST'], endpoint='create_job'
)
@permissions_required(AllPermissionTypes.tools_maintenance)
@pga_login_required
def create_maintenance_job(sid, did):
"""

View File

@ -10,6 +10,7 @@
import getApiInstance from 'sources/api_instance';
import MaintenanceSchema, {getVacuumSchema} from './maintenance.ui';
import { getNodeListByName } from '../../../../browser/static/js/node_ajax';
import { AllPermissionTypes } from '../../../../browser/static/js/constants';
define([
'sources/gettext', 'sources/url_for', 'sources/pgadmin', 'pgadmin.browser',
@ -51,6 +52,7 @@ define([
data: {
data_disabled: gettext('Please select any database from the object explorer to do Maintenance.'),
},
permission: AllPermissionTypes.TOOLS_MAINTENANCE,
}];
// Add supported menus into the menus list

View File

@ -10,7 +10,7 @@
import { getRandomInt, hasBinariesConfiguration } from 'sources/utils';
import { retrieveAncestorOfTypeServer } from 'sources/tree/tree_utils';
import { generateTitle } from 'tools/sqleditor/static/js/sqleditor_title';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
import usePreferences,{ listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import 'pgadmin.browser.keyboard';
import pgWindow from 'sources/window';
@ -93,6 +93,7 @@ export default class Psql {
applies: 'tools',
data_disabled: gettext('Please select a database from the object explorer to access Pql Tool.'),
},
permission: AllPermissionTypes.TOOLS_PSQL_TOOL,
}];

View File

@ -15,7 +15,7 @@ from flask import render_template, request, current_app, Response
from flask_babel import gettext as _
# This unused import is required as API test cases will fail if we remove it,
# Have to identify the cause and then remove it.
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
from pgadmin.misc.bgprocess.processes import BatchProcess, IProcessDesc
from pgadmin.utils import PgAdminModule, fs_short_path, does_utility_exist, \
@ -25,6 +25,7 @@ from pgadmin.utils.ajax import make_json_response, bad_request, \
from config import PG_DEFAULT_DRIVER
from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_NOT_FOUND
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
# set template path for sql scripts
MODULE_NAME = 'restore'
@ -117,19 +118,6 @@ def index():
return bad_request(errormsg=_("This URL cannot be called directly."))
@blueprint.route("/restore.js")
@pga_login_required
def script():
"""render own javascript"""
return Response(
response=render_template(
"restore/js/restore.js", _=_
),
status=200,
mimetype=MIMETYPE_APP_JS
)
def _get_create_req_data():
"""
Get data from request for create restore job.
@ -398,6 +386,7 @@ def use_sql_utility(data, manager, server, filepath):
@blueprint.route('/job/<int:sid>', methods=['POST'], endpoint='create_job')
@permissions_required(AllPermissionTypes.tools_restore)
@pga_login_required
def create_restore_job(sid):
"""

View File

@ -12,6 +12,7 @@ import getApiInstance from 'sources/api_instance';
import {retrieveAncestorOfTypeServer} from 'sources/tree/tree_utils';
import RestoreSchema, {getRestoreSaveOptSchema, getRestoreDisableOptionSchema, getRestoreMiscellaneousSchema, getRestoreTypeObjSchema, getRestoreSectionSchema} from './restore.ui';
import pgAdmin from 'sources/pgadmin';
import { AllPermissionTypes } from '../../../../browser/static/js/constants';
define('tools.restore', [
'sources/gettext', 'sources/url_for', 'pgadmin.browser',
@ -49,6 +50,7 @@ define('tools.restore', [
data: {
data_disabled: gettext('Please select any schema or table from the object explorer to Restore data.'),
},
permission: AllPermissionTypes.TOOLS_RESTORE,
}];
for (let sup_node_val of menuUtils.restoreSupportedNodes) {
@ -63,6 +65,7 @@ define('tools.restore', [
enable: supportedNodes.enabled.bind(
null, pgBrowser.tree, menuUtils.restoreSupportedNodes
),
permission: AllPermissionTypes.TOOLS_RESTORE,
});
}

View File

@ -15,7 +15,7 @@ import copy
from flask import Response, session, url_for, request
from flask import render_template, current_app as app
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.user_login_check import pga_login_required
from flask_babel import gettext
from pgadmin.utils import PgAdminModule
@ -31,6 +31,7 @@ from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS,\
from sqlalchemy import or_
from pgadmin.authenticate import socket_login_required
from pgadmin import socketio
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
MODULE_NAME = 'schema_diff'
COMPARE_MSG = gettext("Comparing objects...")
@ -123,6 +124,8 @@ def index():
methods=["GET"],
endpoint='panel'
)
@permissions_required(AllPermissionTypes.tools_schema_diff)
@pga_login_required
def panel(trans_id, editor_title):
"""
This method calls index.html to render the schema diff.

View File

@ -19,7 +19,7 @@ import getApiInstance from '../../../../static/js/api_instance';
import Theme from '../../../../static/js/Theme';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import SchemaDiffComponent from './components/SchemaDiffComponent';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import pgAdmin from 'sources/pgadmin';
@ -57,6 +57,7 @@ export default class SchemaDiff {
label: gettext('Schema Diff'),
enable: true,
below: true,
permission: AllPermissionTypes.TOOLS_SCHEMA_DIFF,
}]);
}

View File

@ -11,6 +11,7 @@
from flask import request
from flask_babel import gettext
from flask_security import permissions_required
from pgadmin.user_login_check import pga_login_required
from pgadmin.utils import PgAdminModule
@ -18,6 +19,7 @@ from pgadmin.utils.ajax import make_json_response, bad_request,\
internal_server_error
from pgadmin.utils.preferences import Preferences
from pgadmin.tools.search_objects.utils import SearchObjectsHelper
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
MODULE_NAME = 'search_objects'
@ -67,6 +69,7 @@ def types(sid, did):
@blueprint.route("search/<int:sid>/<int:did>", endpoint='search')
@permissions_required(AllPermissionTypes.tools_search_objects)
@pga_login_required
def search(sid, did):
"""

View File

@ -47,6 +47,7 @@ export default class SearchObjectModule {
data: {
data_disabled: gettext('Please select a database from the object explorer to search the database objects.'),
},
permission: 'tools_search_objects',
}];
pgBrowser.add_menus(menus);

View File

@ -29,7 +29,7 @@ from flask_babel import gettext
from pgadmin.tools.sqleditor.utils.query_tool_connection_check \
import query_tool_connection_check
from pgadmin.user_login_check import pga_login_required
from flask_security import current_user
from flask_security import current_user, permissions_required
from pgadmin.misc.file_manager import Filemanager
from pgadmin.tools.sqleditor.command import QueryToolCommand, ObjectRegistry, \
SQLFilter
@ -67,6 +67,7 @@ from pgadmin.settings import get_setting
from pgadmin.utils.preferences import Preferences
from pgadmin.tools.sqleditor.utils.apply_explain_plan_wrapper import \
get_explain_query_length
from pgadmin.tools.user_management.PgAdminPermissions import AllPermissionTypes
from pgadmin.browser.server_groups.servers.utils import \
convert_connection_parameter, get_db_disp_restriction
from pgadmin.misc.workspaces import check_and_delete_adhoc_server
@ -295,6 +296,7 @@ def initialize_viewdata(trans_id, cmd_type, obj_type, sgid, sid, did, obj_id):
methods=["POST"],
endpoint='panel'
)
@pga_login_required
def panel(trans_id):
"""
This method calls index.html to render the data grid.
@ -375,6 +377,7 @@ def panel(trans_id):
'/initialize/sqleditor/<int:trans_id>/<int:sgid>/<int:sid>',
methods=["POST"], endpoint='initialize_sqleditor'
)
@permissions_required(AllPermissionTypes.tools_query_tool)
@pga_login_required
def initialize_sqleditor(trans_id, sgid, sid, did=None):
"""

View File

@ -23,7 +23,7 @@ import ReactDOM from 'react-dom/client';
import QueryToolComponent from './components/QueryToolComponent';
import ModalProvider from '../../../../static/js/helpers/ModalProvider';
import Theme from '../../../../static/js/Theme';
import { BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
import { AllPermissionTypes, BROWSER_PANELS, WORKSPACES } from '../../../../browser/static/js/constants';
import { NotifierProvider } from '../../../../static/js/helpers/Notifier';
import usePreferences, { listenPreferenceBroadcast } from '../../../../preferences/static/js/store';
import { PgAdminProvider } from '../../../../static/js/PgAdminProvider';
@ -104,6 +104,7 @@ export default class SQLEditor {
applies: 'tools',
data_disabled: gettext('Please select a database from the object explorer to access Query Tool.'),
},
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}];
// Create context menu
@ -121,6 +122,7 @@ export default class SQLEditor {
category: 'view_data',
priority: 101,
label: gettext('All Rows'),
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}, {
name: 'view_first_100_rows_context_' + supportedNode,
node: supportedNode,
@ -134,6 +136,7 @@ export default class SQLEditor {
category: 'view_data',
priority: 102,
label: gettext('First 100 Rows'),
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}, {
name: 'view_last_100_rows_context_' + supportedNode,
node: supportedNode,
@ -147,6 +150,7 @@ export default class SQLEditor {
category: 'view_data',
priority: 103,
label: gettext('Last 100 Rows'),
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
}, {
name: 'view_filtered_rows_context_' + supportedNode,
node: supportedNode,
@ -160,6 +164,7 @@ export default class SQLEditor {
category: 'view_data',
priority: 104,
label: gettext('Filtered Rows...'),
permission: AllPermissionTypes.TOOLS_QUERY_TOOL,
});
}

View File

@ -0,0 +1,135 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
from flask_babel import gettext
class AllPermissionTypes:
object_register_server = 'object_register_server'
tools_erd_tool = 'tools_erd_tool'
tools_query_tool = 'tools_query_tool'
tools_debugger = 'tools_debugger'
tools_psql_tool = 'tools_psql_tool'
tools_backup = 'tools_backup'
tools_restore = 'tools_restore'
tools_import_export_data = 'tools_import_export_data'
tools_import_export_servers = 'tools_import_export_servers'
tools_search_objects = 'tools_search_objects'
tools_maintenance = 'tools_maintenance'
tools_schema_diff = 'tools_schema_diff'
tools_grant_wizard = 'tools_grant_wizard'
storage_add_folder = 'storage_add_folder'
storage_remove_folder = 'storage_remove_folder'
@staticmethod
def list():
return filter(lambda x: not x.startswith('_'),
AllPermissionTypes.__dict__.keys())
class AllPermissionCategories:
object_explorer = 'Object Explorer'
tools = 'Tools'
storage_manager = 'Storage Manager'
class PgAdminPermissions:
_all_permissions = []
def __init__(self):
self.add_permission(
AllPermissionCategories.object_explorer,
AllPermissionTypes.object_register_server,
gettext("Register/remove server")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_query_tool,
gettext("Query tool")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_debugger,
gettext("Debugger")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_psql_tool,
gettext("PSQL tool")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_backup,
gettext("Backup tool (including server and globals)")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_restore,
gettext("Restore tool")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_import_export_data,
gettext("Import/export data")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_import_export_servers,
gettext("Import/export servers")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_search_objects,
gettext("Search objects")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_maintenance,
gettext("Maintenance")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_schema_diff,
gettext("Schema diff")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_grant_wizard,
gettext("Grant wizard")
)
self.add_permission(
AllPermissionCategories.tools,
AllPermissionTypes.tools_erd_tool,
gettext("ERD tool")
)
self.add_permission(
AllPermissionCategories.storage_manager,
AllPermissionTypes.storage_add_folder,
gettext("Add folder")
)
self.add_permission(
AllPermissionCategories.storage_manager,
AllPermissionTypes.storage_remove_folder,
gettext("Delete file/folder")
)
def add_permission(self, category: str, permission: str, label: str):
self._all_permissions.append({
"category": category,
"name": permission,
"label": label,
})
@property
def all_permissions(self):
return sorted(
self._all_permissions,
key=lambda x: (
x['category'],
x['label']))

View File

@ -27,17 +27,21 @@ from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_response as ajax_response, \
make_json_response, bad_request, internal_server_error
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL,\
from pgadmin.utils.constants import MIMETYPE_APP_JS, INTERNAL, \
SUPPORTED_AUTH_SOURCES
from pgadmin.utils.validation_utils import validate_email
from pgadmin.model import db, Role, User, UserPreference, Server, \
ServerGroup, Process, Setting, roles_users, SharedServer
from pgadmin.utils.paths import create_users_storage_directory
from pgadmin.tools.user_management.PgAdminPermissions import PgAdminPermissions
from sqlalchemy import func
# set template path for sql scripts
MODULE_NAME = 'user_management'
server_info = {}
permissions_obj = PgAdminPermissions()
class UserManagementModule(PgAdminModule):
"""
@ -62,13 +66,21 @@ class UserManagementModule(PgAdminModule):
list: URL endpoints for backup module
"""
return [
'user_management.roles', 'user_management.role',
'user_management.users', 'user_management.user',
'user_management.roles',
'user_management.role',
'user_management.role_save',
'user_management.role_delete',
'user_management.users',
'user_management.user',
current_app.login_manager.login_view,
'user_management.auth_sources', 'user_management.change_owner',
'user_management.shared_servers', 'user_management.admin_users',
'user_management.save', 'user_management.save_id'
]
'user_management.auth_sources',
'user_management.change_owner',
'user_management.shared_servers',
'user_management.admin_users',
'user_management.save',
'user_management.save_id',
'user_management.all_permissions',
'user_management.save_permissions']
# Create blueprint for BackupModule class
@ -83,35 +95,21 @@ def index():
return bad_request(errormsg=_("This URL cannot be called directly."))
@blueprint.route("/user_management.js")
@pga_login_required
def script():
"""render own javascript"""
return Response(
response=render_template(
"user_management/js/user_management.js", _=_,
is_admin=current_user.has_role("Administrator"),
user_id=current_user.id
),
status=200,
mimetype=MIMETYPE_APP_JS
)
@blueprint.route("/current_user.js")
@pgCSRFProtect.exempt
@pga_login_required
def current_user_info():
current_user.has_permission
return Response(
response=render_template(
"user_management/js/current_user.js",
is_admin='true' if current_user.has_role(
"Administrator") else 'false',
user_id=current_user.id,
email=current_user.email.replace("'","\\'") if current_user.email
email=current_user.email.replace("'", "\\'") if current_user.email
else current_user.email,
name=(
current_user.username.split('@')[0].replace("'","\\'") if
current_user.username.split('@')[0].replace("'", "\\'") if
config.SERVER_MODE is True
else 'postgres'
),
@ -124,8 +122,13 @@ def current_user_info():
session.get('allow_save_password', None) else 'false',
auth_sources=config.AUTHENTICATION_SOURCES,
current_auth_source=session['auth_source_manager'][
'current_source'] if config.SERVER_MODE is True else INTERNAL
'current_source'] if config.SERVER_MODE is True else INTERNAL,
permissions=list({p for r in current_user.roles
for p in r.get_permissions()})
),
headers={
'Cache-Control': 'no-cache, no-store, must-revalidate'
},
status=200,
mimetype=MIMETYPE_APP_JS
)
@ -348,6 +351,81 @@ def admin_users(uid=None):
)
def create_role(data):
try:
validate_unique_role(data)
r = Role(name=data['name'],
description=data['description'])
db.session.add(r)
db.session.commit()
return ajax_response(
status=200
)
except Exception as e:
db.session.rollback()
current_app.logger.exception(e)
return internal_server_error(str(e))
def update_role(rid, data):
try:
validate_unique_role(data)
r = Role.query.get(rid)
if not r:
return ajax_response(
response=_('Role not found'),
status=404
)
for key, value in data.items():
setattr(r, key, value)
db.session.commit()
return ajax_response(
status=200
)
except Exception as e:
db.session.rollback()
current_app.logger.exception(e)
return internal_server_error(str(e))
def delete_role(rid):
r = Role.query.get(rid)
if not r:
return ajax_response(
response=_('Role not found'),
status=404
)
users = User.query.all()
for u in users:
if u.has_role(r):
return make_json_response(
success=0,
status=400,
errormsg=_(
'To proceed, ensure that all users assigned '
'the \'{0}\' role have been reassigned.'.format(r.name))
)
try:
# Finally delete user
db.session.delete(r)
db.session.commit()
return ajax_response(
status=200
)
except Exception as e:
db.session.rollback()
current_app.logger.exception(e)
return internal_server_error(str(e))
@blueprint.route(
'/role/', methods=['GET'], defaults={'rid': None}, endpoint='roles'
)
@ -366,14 +444,21 @@ def role(rid):
if rid:
r = Role.query.get(rid)
res = {'id': r.id, 'name': r.name}
res = {'id': r.id,
'name': r.name,
'description': r.description,
'permissions': r.permissions,
'is_admin': r.name == "Administrator"}
else:
roles = Role.query.all()
roles_data = []
for r in roles:
roles_data.append({'id': r.id,
'name': r.name})
'name': r.name,
'description': r.description,
'permissions': r.permissions,
'is_admin': r.name == "Administrator"})
res = roles_data
@ -383,6 +468,32 @@ def role(rid):
)
@blueprint.route(
'/role/', methods=['POST'], defaults={'id': None}, endpoint='role_save'
)
@blueprint.route('/role/<int:id>', methods=['DELETE'], endpoint='role_delete')
@roles_required('Administrator')
def role_save(id):
"""
Args:
id: Role id
"""
if request.method == 'DELETE':
return delete_role(id)
data = request.form if request.form else json.loads(
request.data
)
if 'id' not in data:
return create_role(data)
else:
return update_role(data['id'], data)
@blueprint.route(
'/auth_sources/', methods=['GET'], endpoint='auth_sources'
)
@ -446,6 +557,18 @@ def normalise_password(password):
normalize(normalise_form, password)
def validate_unique_role(data):
if 'name' not in data:
return
exist_roles = Role.query.filter(
func.lower(Role.name) == func.lower(data['name'])
).count()
if exist_roles != 0:
raise InternalServerError(_("Role name must be unique."))
def validate_password(data, new_data):
"""
Check password new and confirm password match. If both passwords are not
@ -652,3 +775,41 @@ def delete_user(uid):
return False, str(e)
return True, ''
@blueprint.route('/all_permissions',
methods=['GET'],
endpoint='all_permissions')
@roles_required('Administrator')
def get_all_permissions():
return ajax_response(
status=200,
response=permissions_obj.all_permissions
)
@blueprint.route('/save_permissions/<int:id>',
methods=['PUT'], endpoint='save_permissions')
@roles_required('Administrator')
def save_permissions(id):
data = request.form if request.form else json.loads(
request.data
)
r = Role.query.get(id)
try:
r.permissions = data['permissions']
db.session.commit()
except Exception as e:
db.session.rollback()
return internal_server_error(errormsg=str(e))
return ajax_response(
status=200,
response={
'id': r.id,
'name': r.name,
'permissions': r.permissions
}
)

View File

@ -7,10 +7,14 @@
//
//////////////////////////////////////////////////////////////
import React from 'react';
import React, { useEffect } from 'react';
import { Box, styled, Tab, Tabs } from '@mui/material';
import TabPanel from '../../../../static/js/components/TabPanel';
import url_for from 'sources/url_for';
import Users from './Users';
import Permissions from './Permissions';
import getApiInstance from '../../../../static/js/api_instance';
import Roles from './Roles';
const Root = styled('div')(({theme}) => ({
height: '100%',
@ -23,12 +27,33 @@ const Root = styled('div')(({theme}) => ({
flexGrow: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0,
...theme.mixins.panelBorder.all,
}
}));
export default function Component() {
const [tabValue, setTabValue] = React.useState(0);
const [roles, setRoles] = React.useState([]);
const fetchRoles = async () => {
const url = url_for('user_management.roles');
const response = await getApiInstance().get(url);
setRoles(response.data);
};
const updateRolePermissions = (rid, permissions) => {
setRoles(roles.map((r) => {
if (r.id === rid) {
return {...r, permissions};
}
return r;
}));
};
useEffect(() => {
fetchRoles();
}, []);
return (
<Root>
@ -44,10 +69,18 @@ export default function Component() {
action={(ref)=>ref?.updateIndicator()}
>
<Tab label="Users" />
<Tab label="Roles" />
<Tab label="Permissions" />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
<Users />
<Users roles={roles} />
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Roles roles={roles} updateRoles={fetchRoles} />
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Permissions roles={roles} updateRolePermissions={updateRolePermissions} />
</TabPanel>
</Box>
</Root>

View File

@ -0,0 +1,202 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useMemo, useEffect } from 'react';
import url_for from 'sources/url_for';
import gettext from 'sources/gettext';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import { Box, FormLabel } from '@mui/material';
import SectionContainer from '../../../../dashboard/static/js/components/SectionContainer';
import { InputCheckbox, InputSelect, InputText } from '../../../../static/js/components/FormComponents';
import { SearchRounded } from '@mui/icons-material';
import { PgButtonGroup, PgIconButton, PrimaryButton } from '../../../../static/js/components/Buttons';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
import Loader from 'sources/components/Loader';
import SelectAllRoundedIcon from '@mui/icons-material/SelectAllRounded';
import DeselectRoundedIcon from '@mui/icons-material/DeselectRounded';
import PropTypes from 'prop-types';
function PermissionsForRole({sections, selectedPerms, setSelectedPerms}) {
return (
<Box sx={{display: 'flex', flexDirection: 'column', gap: '8px'}}>
{Object.keys(sections).map(section => {
const items = sections[section];
return <SectionContainer key={section} title={
<Box sx={{display: 'flex', justifyContent: 'space-between', alignItems: 'center'}}>
<Box>{section}</Box>
<Box>
<PgButtonGroup>
<PgIconButton
size="xs"
icon={<SelectAllRoundedIcon />}
aria-label="Select All"
title={gettext('Select All')}
onClick={() => {
setSelectedPerms((prev) => {
return Array.from(new Set([...prev, ...items.map(i => i.name)]));
});
}}
></PgIconButton>
<PgIconButton
size="xs"
icon={<DeselectRoundedIcon />}
aria-label="Deselect All"
title={gettext('Deselect All')}
onClick={() => {
setSelectedPerms((prev) => {
return prev.filter((p) => !items.map(i => i.name).includes(p));
});
}}
></PgIconButton>
</PgButtonGroup>
</Box>
</Box>
} style={{minHeight: 0, height: 'auto'}}>
<Box sx={{p: '8px', display: 'grid', gridAutoFlow: 'column', gridTemplateRows: '1fr '.repeat(Math.ceil(items.length/2)), gap: '4px'}}>
{items.map(item => (
<InputCheckbox
key={item.name}
controlProps={{
label: item.label,
'data-name': item.name,
}}
value={selectedPerms.includes(item.name)}
onChange={(e) => {
let val = e.target.checked;
setSelectedPerms((prev) => {
if (val) {
return [...prev, item.name];
} else {
return prev.filter((p) => p !== item.name);
}
});
}}
sx={{widht: 'fit-content'}}
/>
))}
</Box>
</SectionContainer>;
})}
</Box>
);
}
PermissionsForRole.propTypes = {
sections: PropTypes.object,
selectedPerms: PropTypes.array,
setSelectedPerms: PropTypes.func,
};
export default function Permissions({roles, updateRolePermissions}) {
const api = getApiInstance();
const [allPermissions, setAllPermissions] = React.useState([]);
const [searchVal, setSearchVal] = React.useState('');
const [selectedPerms, setSelectedPerms] = React.useState([]);
const [selectedRole, setSelectedRole] = React.useState();
const [loading, setLoading] = React.useState('');
const pgAdmin = usePgAdmin();
const isDirty = useMemo(() => {
return JSON.stringify(roles.find((r)=>r.id === selectedRole)?.permissions.sort() || []) !== JSON.stringify(selectedPerms.sort());
}, [selectedRole, selectedPerms, roles]);
const savePermissions = async () => {
const url = url_for('user_management.save_permissions', {id: selectedRole});
try {
setLoading(gettext('Saving...'));
const resp = await api.put(url, {permissions: selectedPerms});
updateRolePermissions(selectedRole, resp.data.permissions);
pgAdmin.Browser.notifier.success(gettext('Permissions saved successfully'));
} catch (error) {
pgAdmin.Browser.notifier.error(parseApiError(error));
console.error(error);
}
setLoading('');
};
useEffect(() => {
const url = url_for('user_management.all_permissions');
api.get(url)
.then(response => {
setAllPermissions(response.data);
})
.catch(error => {
pgAdmin.Browser.notifier.error(parseApiError(error));
console.error(error);
});
}, []);
useEffect(() => {
setSelectedPerms(roles.find((r)=>r.id === selectedRole)?.permissions || []);
}, [selectedRole]);
useEffect(() => {
if (selectedRole) {
const role = roles.find((r)=>r.id === selectedRole);
if (!role) {
setSelectedRole(undefined);
}
}
}, [roles]);
const filteredAllPermissions = useMemo(() => {
return allPermissions.filter(perm => perm.label.toLowerCase().includes(searchVal.toLowerCase()));
}, [allPermissions, searchVal]);
// Convert the permissions array to section based dict
const sections = useMemo(()=>{
return filteredAllPermissions.reduce((acc, perm) => {
let section = perm.category;
if (!acc[section]) {
acc[section] = [];
}
acc[section].push(perm);
return acc;
}, {});
}, [filteredAllPermissions]);
return (
<Box sx={{display: 'flex', flexDirection: 'column', gap: '4px', position: 'relative', height: '100%'}}>
<Loader message={loading} />
<Box sx={{display: 'flex', gap: '4px', alignItems: 'center'}}>
<FormLabel>{gettext('Role')}</FormLabel>
<Box sx={{minWidth: '300px'}}>
<InputSelect
options={roles.filter((r)=>r.name != 'Administrator').map((r) => ({ label: r.name, value: r.id }))}
optionsReloadBasis={roles.map((r)=>r.name).join('')}
onChange={(val) => {setSelectedRole(val);}}
value={selectedRole}
placeholder={gettext('Select Role')}
/>
</Box>
<PrimaryButton disabled={!isDirty||Boolean(loading)} onClick={savePermissions}>{gettext('Save')}</PrimaryButton>
<Box sx={{marginLeft: 'auto', minWidth: '300px'}}>
<InputText
placeholder={gettext('Search')}
controlProps={{ title: gettext('Search') }}
value={searchVal}
onChange={(val) => {
setSearchVal(val);
}}
startAdornment={<SearchRounded />}
/>
</Box>
</Box>
{selectedRole &&
<Box sx={{overflowY: 'auto', flexGrow: 1}}>
<PermissionsForRole sections={sections} selectedPerms={selectedPerms} setSelectedPerms={setSelectedPerms}/>
</Box>}
</Box>
);
}
Permissions.propTypes = {
roles: PropTypes.array.isRequired,
updateRolePermissions: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,84 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useMemo } from 'react';
import SchemaView from '../../../../static/js/SchemaView';
import BaseUISchema from '../../../../static/js/SchemaView/base_schema.ui';
import gettext from 'sources/gettext';
import url_for from 'sources/url_for';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import ErrorBoundary from '../../../../static/js/helpers/ErrorBoundary';
import PropTypes from 'prop-types';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
class RoleSchema extends BaseUISchema {
constructor() {
super({
name: '',
});
}
get baseFields() {
return [
{
id: 'name', label: gettext('Name'), type: 'text', noEmpty: true, maxLength: 128,
},
{
id: 'description', label: gettext('Description'), type: 'multiline', noEmpty: true, maxLength: 256,
}
];
}
}
export default function RoleDialog({role, onClose}) {
const pgAdmin = usePgAdmin();
const schema = useMemo(() => new RoleSchema(), []);
const isEdit = Boolean(role.id);
const api = getApiInstance();
const onSaveClick = (_isNew, changeData)=>{
return new Promise((resolve, reject)=>{
try {
api.post(url_for('user_management.role_save'), changeData)
.then(()=>{
pgAdmin.Browser.notifier.success(gettext('Role Saved Successfully'));
resolve();
onClose(null, true);
})
.catch((err)=>{
reject(err instanceof Error ? err : Error(gettext('Something went wrong')));
});
} catch (error) {
reject(Error(parseApiError(error)));
}
});
};
return <ErrorBoundary>
<SchemaView
formType={'dialog'}
getInitData={()=>{ return Promise.resolve(role); }}
schema={schema}
viewHelperProps={{
mode: isEdit ? 'edit' : 'create',
}}
onSave={onSaveClick}
onClose={onClose}
hasSQL={false}
disableSqlHelp={true}
disableDialogHelp={true}
isTabView={false}
/>
</ErrorBoundary>;
}
RoleDialog.propTypes = {
role: PropTypes.object,
onClose: PropTypes.func,
};

View File

@ -0,0 +1,177 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React, { useMemo } from 'react';
import gettext from 'sources/gettext';
import PgTable from '../../../../static/js/components/PgTable';
import { getDeleteCell, getEditCell } from '../../../../static/js/components/PgReactTableStyled';
import RoleDialog from './RoleDialog';
import Loader from 'sources/components/Loader';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import url_for from 'sources/url_for';
import { BROWSER_PANELS } from '../../../../browser/static/js/constants';
import ErrorBoundary from '../../../../static/js/helpers/ErrorBoundary';
import { Box } from '@mui/material';
import {Add as AddIcon, SyncRounded, Help as HelpIcon} from '@mui/icons-material';
import PropTypes from 'prop-types';
import { PgButtonGroup, PgIconButton } from '../../../../static/js/components/Buttons';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
function CustomHeader({updateRoles, pgAdmin}) {
return (
<Box>
<PgButtonGroup>
<PgIconButton
icon={<AddIcon style={{ height: '1.4rem' }} />}
aria-label="Create Role"
title={gettext('Create Role...')}
onClick={() => {
const panelTitle = gettext('Create Role');
const panelId = BROWSER_PANELS.USER_MANAGEMENT + '-new-role';
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
content: (
<ErrorBoundary>
<RoleDialog
role={{}}
onClose={(_e, reload) => {
pgAdmin.Browser.docker.default_workspace.close(panelId, true);
reload && updateRoles();
}}
/>
</ErrorBoundary>
)
}, pgAdmin.Browser.stdW.md, pgAdmin.Browser.stdH.md);
}}
></PgIconButton>
<PgIconButton
icon={<SyncRounded style={{ height: '1.4rem' }} />}
aria-label="Refresh"
title={gettext('Refresh')}
onClick={updateRoles}
></PgIconButton>
<PgIconButton
icon={<HelpIcon style={{ height: '1.4rem' }} />}
aria-label="Help"
title={gettext('Help')}
onClick={() => {
window.open(url_for('help.static', { 'filename': 'user_management.html' }));
}}
></PgIconButton>
</PgButtonGroup>
</Box>
);
}
CustomHeader.propTypes = {
updateRoles: PropTypes.func,
pgAdmin: PropTypes.object,
};
export default function Roles({roles, updateRoles}) {
const [loading, setLoading] = React.useState('');
const api = getApiInstance();
const pgAdmin = usePgAdmin();
const onDeleteClick = (row) => {
pgAdmin.Browser.notifier.confirm(gettext('Delete Role'), gettext('Are you sure you want to delete the role %s?', row.original.name),
async () => {
setLoading(gettext('Deleting role...'));
try {
await api.delete(url_for('user_management.role_delete', { id: row.original.id }));
pgAdmin.Browser.notifier.success(gettext('Role deleted successfully.'));
updateRoles();
} catch (error) {
pgAdmin.Browser.notifier.error(parseApiError(error));
}
setLoading('');
});
};
const onEditClick = (row) => {
const role = row.original;
const panelTitle = gettext('Edit Role - %s', role.name);
const panelId = BROWSER_PANELS.USER_MANAGEMENT + '-edit-role' + role.id;
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
content: (
<ErrorBoundary>
<RoleDialog
role={role}
onClose={(_e, reload) => {
pgAdmin.Browser.docker.default_workspace.close(panelId, true);
reload && updateRoles();
}}
/>
</ErrorBoundary>
)
}, pgAdmin.Browser.stdW.md, pgAdmin.Browser.stdH.md);
};
const columns = useMemo(() => [{
header: () => null,
enableSorting: false,
enableResizing: false,
enableFilters: false,
size: 35,
maxSize: 35,
minSize: 35,
id: 'btn-delete',
cell: getDeleteCell({ title: gettext('Delete Role'), onClick: onDeleteClick, isDisabled: (row) => row.original.is_admin }),
},{
header: () => null,
enableSorting: false,
enableResizing: false,
enableFilters: false,
size: 35,
maxSize: 35,
minSize: 35,
id: 'btn-edit',
cell: getEditCell({ title: gettext('Edit Role'), onClick: onEditClick, isDisabled: (row) => row.original.is_admin }),
},
{
header: gettext('Name'),
accessorKey: 'name',
size: 50,
minSize: 50,
},
{
header: gettext('Decscription'),
accessorKey: 'description',
size: 100,
minSize: 100,
}], []);
return (
<Box sx={{position: 'relative', height: '100%'}}>
<Loader message={loading} />
<PgTable
data-test="roles"
columns={columns}
data={roles}
sortOptions={[{ id: 'name', desc: false }]}
caveTable={false}
tableNoBorder={false}
tableProps={{
getRowId: (row) => {
return row.id;
}
}}
customHeader={<CustomHeader updateRoles={updateRoles} pgAdmin={pgAdmin} />}
></PgTable>
</Box>
);
}
Roles.propTypes = {
roles: PropTypes.array,
updateRoles: PropTypes.func,
};

View File

@ -10,7 +10,6 @@
import React, { useEffect, useMemo, useRef } from 'react';
import { getDeleteCell, getEditCell, getSwitchCell } from '../../../../static/js/components/PgReactTableStyled';
import gettext from 'sources/gettext';
import pgAdmin from 'sources/pgadmin';
import getApiInstance, { parseApiError } from '../../../../static/js/api_instance';
import PgTable from 'sources/components/PgTable';
import url_for from 'sources/url_for';
@ -24,8 +23,9 @@ import PropTypes from 'prop-types';
import { PgButtonGroup, PgIconButton } from '../../../../static/js/components/Buttons';
import { showChangeOwnership } from '../../../../static/js/Dialogs';
import { isEmptyString } from '../../../../static/js/validators';
import { usePgAdmin } from '../../../../static/js/PgAdminProvider';
function CustomHeader({updateUsers, options}) {
function CustomHeader({updateUsers, options, pgAdmin}) {
return (
<Box>
<PgButtonGroup>
@ -77,15 +77,15 @@ function CustomHeader({updateUsers, options}) {
CustomHeader.propTypes = {
updateUsers: PropTypes.func,
options: PropTypes.object,
pgAdmin: PropTypes.object,
};
export default function Users() {
export default function Users({roles}) {
const authSources = useRef([]);
const roles = useRef([]);
const [loading, setLoading] = React.useState('');
const [tableData, setTableData] = React.useState([]);
const [selectedRows, setSelectedRows] = React.useState({});
const api = getApiInstance();
const pgAdmin = usePgAdmin();
const onDeleteClick = (row) => {
const deleteRow = async () => {
@ -144,6 +144,7 @@ export default function Users() {
const user = row.original;
const panelTitle = gettext('Edit User - %s', user.username);
const panelId = BROWSER_PANELS.USER_MANAGEMENT + '-edit-' + user.id;
pgAdmin.Browser.docker.default_workspace.openDialog({
id: panelId,
title: panelTitle,
@ -152,7 +153,7 @@ export default function Users() {
<UserDialog
options={{
authSources: authSources.current.map((s) => ({ label: s.label, value: s.value })),
roles: roles.current.map((r) => ({ label: r.name, value: r.id })),
roles: roles.map((r) => ({ label: r.name, value: r.id })),
}}
user={user}
onClose={(_e, reload) => {
@ -216,7 +217,7 @@ export default function Users() {
},
{
header: gettext('Role'),
accessorFn: (row) => roles.current.find((r)=>r.id == row.role).name,
accessorFn: (row) => roles.find((r)=>r.id == row.role)?.name,
enableSorting: true,
enableResizing: true,
size: 100,
@ -243,7 +244,7 @@ export default function Users() {
enableFilters: true,
cell: getSwitchCell(),
}];
}, []);
}, [roles]);
const updateList = async () => {
setLoading(gettext('Fetching users...'));
@ -259,12 +260,8 @@ export default function Users() {
const initialize = async () => {
setLoading(gettext('Loading...'));
try {
const res = await Promise.all([
api.get(url_for('user_management.auth_sources')),
api.get(url_for('user_management.roles')),
]);
authSources.current = res[0].data;
roles.current = res[1].data;
const res = await api.get(url_for('user_management.auth_sources'));
authSources.current = res.data;
updateList();
} catch (error) {
setLoading('');
@ -284,8 +281,6 @@ export default function Users() {
columns={columns}
data={tableData}
sortOptions={[{ id: 'username', desc: true }]}
selectedRows={selectedRows}
setSelectedRows={setSelectedRows}
caveTable={false}
tableNoBorder={false}
tableProps={{
@ -295,9 +290,13 @@ export default function Users() {
}}
customHeader={<CustomHeader updateUsers={updateList} options={{
authSources: authSources.current.map((s) => ({ label: s.label, value: s.value })),
roles: roles.current.map((r) => ({ label: r.name, value: r.id })),
}} />}
roles: roles.map((r) => ({ label: r.name, value: r.id })),
}} pgAdmin={pgAdmin} />}
></PgTable>
</Box>
);
}
}
Users.propTypes = {
roles: PropTypes.array,
};

View File

@ -16,6 +16,7 @@ define('pgadmin.user_management.current_user', [], function() {
'allow_save_password': {{ allow_save_password }},
'allow_save_tunnel_password': {{ allow_save_tunnel_password }},
'auth_sources': {{ auth_sources }},
'current_auth_source': '{{ current_auth_source }}'
'current_auth_source': '{{ current_auth_source }}',
'permissions': {{ permissions }}
}
});

View File

@ -42,4 +42,6 @@ module.exports = {
'user_management.auth_sources': '/user_management/auth_sources',
'user_management.roles': '/user_management/roles',
'user_management.users': '/user_management/users',
'user_management.all_permissions': '/user_management/all_permissions',
'user_management.save_permissions': '/user_management/save_permissions',
};

View File

@ -0,0 +1,113 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { act, render, screen, fireEvent, waitFor } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { withBrowser } from '../genericFunctions';
import Permissions from '../../../pgadmin/tools/user_management/static/js/Permissions';
import { withTheme } from '../fake_theme';
describe('Permissions Component', () => {
let networkMock;
let ctrl;
const PermissionsWithBrowser = withBrowser(withTheme(Permissions));
const mockRoles = [
{ id: 1, name: 'Administrator', permissions: [] },
{ id: 2, name: 'User', permissions: ['p1', 'p2', 'p3'] },
{ id: 3, name: 'Other', permissions: ['p1', 'p2'] },
];
const mockPermissions = [
{ name: 'p1', label: 'Permission 1', category: 'Category 1' },
{ name: 'p2', label: 'Permission 2', category: 'Category 1' },
{ name: 'p3', label: 'Permission 3', category: 'Category 2' },
];
const mockUpdateRolePermissions = jest.fn();
const renderComponent = async () => {
await act( async () => {
if(ctrl) {
ctrl.unmount();
}
ctrl = render(
<PermissionsWithBrowser
roles={mockRoles}
updateRolePermissions={mockUpdateRolePermissions}
/>
);
});
};
beforeEach(async ()=>{
networkMock = new MockAdapter(axios);
networkMock.onGet('/user_management/all_permissions').reply(200, mockPermissions);
await renderComponent();
});
afterEach(() => {
networkMock.restore();
});
it('renders the component and loads permissions', async () => {
expect(screen.getByText('Role')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
});
it('allows selecting a role and displays permissions', async () => {
fireEvent.focus(screen.getByRole('combobox'));
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown', code: 40 });
fireEvent.click(screen.getByText('Other'));
await waitFor(() => {
expect(screen.getByText('Category 1')).toBeInTheDocument();
expect(screen.getByText('Permission 1')).toBeInTheDocument();
expect(screen.getByText('Permission 2')).toBeInTheDocument();
});
});
it('filters permissions based on search input', async () => {
fireEvent.focus(screen.getByRole('combobox'));
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown', code: 40 });
fireEvent.click(screen.getByText('Other'));
fireEvent.change(screen.getByPlaceholderText('Search'), { target: { value: 'Permission 3' } });
await waitFor(() => {
expect(screen.getByText('Permission 3')).toBeInTheDocument();
expect(screen.queryByText('Permission 1')).not.toBeInTheDocument();
});
});
it('saves permissions', async () => {
fireEvent.focus(screen.getByRole('combobox'));
fireEvent.keyDown(screen.getByRole('combobox'), { key: 'ArrowDown', code: 40 });
fireEvent.click(screen.getByText('Other'));
fireEvent.click(screen.getByText('Permission 3'));
networkMock.onPut('/user_management/save_permissions').reply(200, {
permissions: ['p1', 'p2', 'p3']
});
await waitFor(() => {
expect(screen.getByText('Save')).not.toBeDisabled();
});
await act(async () => {
fireEvent.click(screen.getByText('Save'));
mockRoles[2].permissions = ['p1', 'p2', 'p3'];
});
await renderComponent();
await waitFor(() => {
expect(screen.getByText('Save')).toBeDisabled();
expect(mockUpdateRolePermissions).toHaveBeenCalledWith(3, ['p1', 'p2', 'p3']);
});
});
});

View File

@ -0,0 +1,67 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import React from 'react';
import { act, render } from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { withBrowser } from '../genericFunctions';
import Roles from '../../../pgadmin/tools/user_management/static/js/Roles';
describe('Roles', () => {
let networkMock;
beforeEach(() => {
networkMock = new MockAdapter(axios);
networkMock.onGet('/user_management/auth_sources').reply(200, [
{ id: 1, label: 'internal', value: 'internal' },
]);
networkMock.onGet('/user_management/roles').reply(200, [
{ id: 1, name: 'Administrator' },
{ id: 2, name: 'User' },
]);
networkMock.onGet('/user_management/users').reply(200, [
{ id: 1, label: 'postgres', value: 'postgres', auth_source: 'internal', role: 1 },
]);
});
afterEach(() => {
networkMock.restore();
});
describe('Component', () => {
const RolesWithBrowser = withBrowser(Roles);
it('init', async () => {
let ctrl;
await act(() => {
ctrl = render(<RolesWithBrowser roles={[
{ id: 1, name: 'Administrator' },
{ id: 2, name: 'User' },
]}/>);
});
expect(ctrl.container.querySelectorAll('[data-test="roles"]').length).toBe(1);
});
it('renders role list', async () => {
let ctrl;
await act(() => {
ctrl = render(<RolesWithBrowser roles={[
{ id: 1, name: 'Administrator' },
{ id: 2, name: 'User' },
]}/>);
});
const roleItems = ctrl.container.querySelectorAll('.pgrt-row-content ');
expect(roleItems.length).toBe(2);
expect(roleItems[0].querySelector('.pgrd-row-cell:not(.btn-cell) .pgrd-row-cell-content').textContent).toContain('Administrator');
expect(roleItems[1].querySelector('.pgrd-row-cell:not(.btn-cell) .pgrd-row-cell-content').textContent).toContain('User');
});
});
});

View File

@ -44,8 +44,11 @@ describe('Users', ()=>{
it('init', async ()=>{
let ctrl;
await act(async ()=>{
ctrl = await render(<UsersWithBrowser />);
await act(()=>{
ctrl = render(<UsersWithBrowser roles={[
{ id: 1, name: 'Administrator' },
{ id: 2, name: 'User' },
]}/>);
});
expect(ctrl.container.querySelectorAll('[data-test="users"]').length).toBe(1);
});

View File

@ -37,7 +37,7 @@ if 'SERVER_MODE' in globals():
else:
builtins.SERVER_MODE = None
from pgadmin.model import db, Version, User, \
from pgadmin.model import db, Version, User, Role, \
SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
from pgadmin import create_app
from pgadmin.utils import clear_database_servers, dump_database_servers, \
@ -148,13 +148,27 @@ class AuthType(str, Enum):
internal = INTERNAL
class ManageRoles:
@staticmethod
def get_role(role: str):
app = create_app(config.APP_NAME + '-cli')
usr = None
with app.test_request_context():
usr = Role.query.filter_by(name=role).first()
if not usr:
return None
return usr.id
class ManageUsers:
@app.command()
@update_sqlite_path
def add_user(email: str, password: str,
role: Annotated[Optional[bool], typer.Option(
"--admin/--nonadmin")] = False,
admin: Annotated[Optional[bool],
typer.Option("--admin")] = False,
role: Optional[str] = None,
active: Annotated[Optional[bool],
typer.Option("--active/--inactive")] = True,
console: Optional[bool] = True,
@ -165,7 +179,7 @@ class ManageUsers:
data = {
'email': email,
'role': 1 if role else 2,
'role': 'Administrator' if admin else role,
'active': active,
'auth_source': INTERNAL,
'newPassword': password,
@ -178,9 +192,9 @@ class ManageUsers:
def add_external_user(username: str,
auth_source: AuthExtTypes = AuthExtTypes.oauth2,
email: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option(
"--admin/--nonadmin")] = False,
admin: Annotated[Optional[bool],
typer.Option("--admin")] = False,
role: Optional[str] = None,
active: Annotated[Optional[bool],
typer.Option(
"--active/--inactive")] = True,
@ -194,7 +208,7 @@ class ManageUsers:
data = {
'username': username,
'email': email,
'role': 1 if role else 2,
'role': 'Administrator' if admin else role,
'active': active,
'auth_source': auth_source
}
@ -231,9 +245,9 @@ class ManageUsers:
@update_sqlite_path
def update_user(email: str,
password: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option("--admin/--nonadmin"
)] = None,
admin: Annotated[Optional[bool], typer.Option(
"--admin")] = False,
role: Optional[str] = None,
active: Annotated[Optional[bool],
typer.Option("--active/--inactive"
)] = None,
@ -254,12 +268,21 @@ class ManageUsers:
data['confirmPassword'] = password
if role is not None:
data['role'] = 1 if role else 2
data['role'] = role
if admin:
data['role'] = 'Administrator'
if active is not None:
data['active'] = active
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
rid = ManageRoles.get_role(data['role'])
if rid is None:
print("Role '{0}' does not exists.".format(data['role']))
exit()
data['role'] = rid
uid = ManageUsers.get_user(username=email,
auth_source=INTERNAL)
if not uid:
@ -308,7 +331,7 @@ class ManageUsers:
'username': u.username,
'email': u.email,
'active': u.active,
'role': u.roles[0].id,
'role': u.roles[0].name,
'auth_source': u.auth_source,
'locked': u.locked
}
@ -323,9 +346,9 @@ class ManageUsers:
def update_external_user(username: str,
auth_source: AuthExtTypes = AuthExtTypes.oauth2,
email: Optional[str] = None,
role: Annotated[Optional[bool],
typer.Option("--admin/--nonadmin"
)] = None,
admin: Annotated[Optional[bool], typer.Option(
"--admin")] = False,
role: Optional[str] = None,
active: Annotated[
Optional[bool],
typer.Option("--active/--inactive")] = None,
@ -340,12 +363,21 @@ class ManageUsers:
if email:
data['email'] = email
if role is not None:
data['role'] = 1 if role else 2
data['role'] = role
if admin:
data['role'] = 'Administrator'
if active is not None:
data['active'] = active
app = create_app(config.APP_NAME + '-cli')
with app.test_request_context():
rid = ManageRoles.get_role(data['role'])
if rid is None:
print("Role '{0}' does not exists.".format(data['role']))
exit()
data['role'] = rid
uid = ManageUsers.get_user(username=username,
auth_source=auth_source)
if not uid:
@ -367,6 +399,14 @@ class ManageUsers:
with app.test_request_context():
username = data['username'] if 'username' in data else \
data['email']
rid = ManageRoles.get_role(data['role'])
if rid is None:
print("Role '{0}' does not exists.".format(data['role']))
exit()
data['role'] = rid
uid = ManageUsers.get_user(username=username,
auth_source=data['auth_source'])
if uid:
@ -379,7 +419,10 @@ class ManageUsers:
status, msg = create_user(data)
if status:
ManageUsers.display_user(data, console, json)
_user = ManageUsers.get_users_from_db(
username=data['email'],
auth_source=data['auth_source'],
console=console)
else:
print(SOMETHING_WENT_WRONG + str(msg))
@ -412,10 +455,7 @@ class ManageUsers:
if 'email' in _data:
table.add_row("Email", _data['email'])
table.add_row("auth_source", _data['auth_source'])
table.add_row("role",
"Admin" if _data['role'] and
_data['role'] != 2 else
"Non-admin")
table.add_row("role", _data['role'])
table.add_row("active",
'True' if _data['active'] else 'False')
console.print(table)