Add support for custom roles and role permissions management in pgAdmin. #7310
parent
8b4df8beb1
commit
7d8a915ee0
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
|
|
@ -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
|
||||
***********
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ define('pgadmin.browser', [
|
|||
checked: _m.checked,
|
||||
below: _m.below,
|
||||
applies: _m.applies,
|
||||
permission: _m.permission,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}]);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 don’t have the necessary permissions to access this feature. Please contact your administrator for assistance')
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -873,4 +873,4 @@ FileManager.propTypes = {
|
|||
onCancel: PropTypes.func,
|
||||
sharedStorages: PropTypes.array,
|
||||
restrictedSharedStorage: PropTypes.array,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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']))
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
84
web/setup.py
84
web/setup.py
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue