diff --git a/docs/en_US/images/role_membership.png b/docs/en_US/images/role_membership.png index 9c42c83e1..c1e07fee9 100644 Binary files a/docs/en_US/images/role_membership.png and b/docs/en_US/images/role_membership.png differ diff --git a/docs/en_US/release_notes_5_1.rst b/docs/en_US/release_notes_5_1.rst index f0a0c9e5a..48e60526a 100644 --- a/docs/en_US/release_notes_5_1.rst +++ b/docs/en_US/release_notes_5_1.rst @@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o New features ************ +| `Issue #5404 `_ - Show the login roles that are members of a group role be shown when examining a group role. | `Issue #6268 `_ - Make 'kerberos' an optional feature in the Python wheel, to avoid the need to install MIT Kerberos on the system by default. | `Issue #6270 `_ - Added '--replace' option in Import server to replace the list of servers with the newly imported one. diff --git a/docs/en_US/role_dialog.rst b/docs/en_US/role_dialog.rst index 9cb1c1f73..5c10c973c 100644 --- a/docs/en_US/role_dialog.rst +++ b/docs/en_US/role_dialog.rst @@ -71,11 +71,11 @@ Use the *Privileges* tab to grant privileges to the role. :alt: Role dialog membership tab :align: center -* Specify members of the role in the *Role Membership* field. Click inside the - *Roles* field to select role names from a drop down list. Confirm each - selection by checking the checkbox to the right of the role name; delete a - selection by clicking the *x* to the left of the role name. Membership conveys - the privileges granted to the specified role to each of its members. +* Specify member of the role in the *Member of* field and specify the members in the *Member* field. + Confirm each selection by checking the checkbox to the right of the role name; + delete a selection by clicking the *x* to the left of the role name. + Membership conveys the privileges granted to the specified role to each of + its members. Click the *Parameters* tab to continue. diff --git a/web/pgadmin/browser/server_groups/servers/roles/__init__.py b/web/pgadmin/browser/server_groups/servers/roles/__init__.py index ca08b9791..28fe6be2c 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/roles/__init__.py @@ -236,6 +236,105 @@ class RoleView(PGChildNodeView): data['rolmembership'].get('deleted', []), lambda _: True, 'role') + def _process_rolmembers(self, id, data): + """ + Parser role members. + :param id: + :param data: + """ + def _part_dict_list(dict_list, condition, list_key=None): + ret_val = [] + for d in dict_list: + if condition(d): + ret_val.append(d[list_key]) + + return ret_val + if id == -1: + data['rol_members'] = [] + data['rol_admins'] = [] + + data['rol_admins'] = _part_dict_list( + data['rolmembers'], lambda d: d['admin'], 'role') + data['rol_members'] = _part_dict_list( + data['rolmembers'], lambda d: not d['admin'], 'role') + else: + data['rol_admins'] = _part_dict_list( + data['rolmembers'].get('added', []), + lambda d: d['admin'], 'role') + data['rol_members'] = _part_dict_list( + data['rolmembers'].get('added', []), + lambda d: not d['admin'], 'role') + + data['rol_admins'].extend(_part_dict_list( + data['rolmembers'].get('changed', []), + lambda d: d['admin'], 'role')) + data['rol_revoked_admins'] = _part_dict_list( + data['rolmembers'].get('changed', []), + lambda d: not d['admin'], 'role') + + data['rol_revoked'] = _part_dict_list( + data['rolmembers'].get('deleted', []), + lambda _: True, 'role') + + def _validate_rolemembers(self, id, data): + """ + Validate the rolmembers data dict + :param data: role data + :return: valid or invalid message + """ + if 'rolmembers' not in data: + return None + + if id == -1: + msg = _(""" +Role members information must be passed as an array of JSON objects in the +following format: + +rolmembers:[{ + role: [rolename], + admin: True/False + }, + ... +]""") + + if not self._validate_input_dict_for_new(data['rolmembers'], + ['role', 'admin']): + return msg + + self._process_rolmembers(id, data) + return None + + msg = _(""" +Role membership information must be passed as a string representing an array of +JSON objects in the following format: +rolmembers:{ + 'added': [{ + role: [rolename], + admin: True/False + }, + ... + ], + 'deleted': [{ + role: [rolename], + admin: True/False + }, + ... + ], + 'updated': [{ + role: [rolename], + admin: True/False + }, + ... + ] +""") + if not self._validate_input_dict_for_update(data['rolmembers'], + ['role', 'admin'], + ['role']): + return msg + + self._process_rolmembers(id, data) + return None + def _validate_rolemembership(self, id, data): """ Validate the rolmembership data dict @@ -433,7 +532,7 @@ rolmembership:{ 'rolcanlogin', 'rolsuper', 'rolcreatedb', 'rolcreaterole', 'rolinherit', 'rolreplication', 'rolcatupdate', 'variables', 'rolmembership', - 'seclabels' + 'seclabels', 'rolmembers' ]: data[key] = json.loads(val, encoding='utf-8') else: @@ -466,6 +565,11 @@ rolmembership:{ if invalid_msg is not None: return precondition_required(invalid_msg) + invalid_msg = self._validate_rolemembers( + kwargs.get('rid', -1), data) + if invalid_msg is not None: + return precondition_required(invalid_msg) + self.request = data return f(self, **kwargs) @@ -716,6 +820,16 @@ rolmembership:{ }) row['seclabels'] = res + if 'rolmembers' in row: + rolmembers = [] + for role in row['rolmembers']: + role = re.search(r'([01])(.+)', role) + rolmembers.append({ + 'role': role.group(2), + 'admin': True if role.group(1) == '1' else False + }) + row['rolmembers'] = rolmembers + @check_precondition(action='properties') def properties(self, gid, sid, rid): diff --git a/web/pgadmin/browser/server_groups/servers/roles/static/js/role.js b/web/pgadmin/browser/server_groups/servers/roles/static/js/role.js index 6c382854f..9bfc2b26c 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/static/js/role.js +++ b/web/pgadmin/browser/server_groups/servers/roles/static/js/role.js @@ -90,7 +90,7 @@ define('pgadmin.node.role', [ template: _.template([ '', '
', - ' multiple="multiple" style="width:100%;" class="pgadmin-controls <%=extraClasses.join(\' \')%>" name="<%=name%>" value="<%-JSON.stringify(value)%>" <%=disabled ? "disabled" : ""%> <%=readonly ? "readonly" : ""%> <%=required ? "required" : ""%>>', ' <% for (var i=0; i < options.length; i++) { %>', ' <% var option = options[i]; %>', ' ', @@ -107,7 +107,7 @@ define('pgadmin.node.role', [ ' <%= opttext %>', ' <% if (checkbox) { %>', '
', - ' ', + ' />', ' ', @@ -149,6 +149,7 @@ define('pgadmin.node.role', [ _.extend(data, { disabled: evalF(data.disabled, data, this.model), visible: evalF(data.visible, data, this.model), + readonly: evalF(data.readonly, data, this.model), required: evalF(data.required, data, this.model), helpMessage: evalASFunc(data.helpMessage, data, this.model), }); @@ -242,7 +243,7 @@ define('pgadmin.node.role', [ multiple: true, tags: true, allowClear: data.disabled ? false : true, - placeholder: data.disabled ? '' : gettext('Select members'), + placeholder: data.disabled ? '' : gettext('Select roles'), width: 'style', }).on('change', function(e) { $(e.target).find(':selected').each(function() { @@ -386,6 +387,7 @@ define('pgadmin.node.role', [ rolcatupdate: false, rolreplication: false, rolmembership: [], + rolmembers: [], rolvaliduntil: null, seclabels: [], variables: [], @@ -492,11 +494,12 @@ define('pgadmin.node.role', [ controlsClassName: 'pgadmin-controls pg-el-sm-8 pg-el-12', min_version: 90100, readonly: 'readonly', - },{ - id: 'rolmembership', label: gettext('Roles'), + }, + { + id: 'rolmembership', label: gettext('Member of'), group: gettext('Membership'), type: 'collection', - cell: 'string', readonly: 'readonly', - mode: ['properties', 'edit', 'create'], + cell: 'string', mode: ['properties', 'edit', 'create'], + readonly: 'readonly', control: RoleMembersControl, model: pgBrowser.Node.Model.extend({ keys: ['role'], idAttribute: 'role', @@ -518,7 +521,34 @@ define('pgadmin.node.role', [ return gettext('Roles shown with a check mark have the WITH ADMIN OPTION set.'); } }, - },{ + }, + { + id: 'rolmembers', label: gettext('Members'), type: 'collection', group: gettext('Membership'), + mode: ['properties', 'edit', 'create'], cell: 'string', + readonly: 'readonly', + control: RoleMembersControl, model: pgBrowser.Node.Model.extend({ + keys: ['role'], + idAttribute: 'role', + defaults: { + role: undefined, + admin: false, + }, + validate: function() { + return null; + }, + }), + filter: function(d) { + return this.model.isNew() || (this.model.get('rolname') != d.label); + }, + helpMessage: function(m) { + if (m.has('read_only') && m.get('read_only') == false) { + return gettext('Select the checkbox for roles to include WITH ADMIN OPTION.'); + } else { + return gettext('Roles shown with a check mark have the WITH ADMIN OPTION set.'); + } + }, + }, + { id: 'variables', label: '', type: 'collection', group: gettext('Parameters'), hasDatabase: true, url: 'variables', model: pgBrowser.Node.VariableModel.extend({keys:['name', 'database']}), diff --git a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/properties.sql index d7d90dd25..2e653746c 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/properties.sql @@ -9,6 +9,15 @@ SELECT LEFT JOIN pg_catalog.pg_roles rm ON (rm.oid = am.roleid) ) rolmembership, (SELECT pg_catalog.array_agg(provider || '=' || label) FROM pg_catalog.pg_shseclabel sl1 WHERE sl1.objoid=r.oid) AS seclabels + {% if rid %} + ,ARRAY( + SELECT + CASE WHEN pg.admin_option THEN '1' ELSE '0' END || pg.usename + FROM + (SELECT pg_roles.rolname AS usename, pg_auth_members.admin_option AS admin_option FROM pg_roles + JOIN pg_auth_members ON pg_roles.oid=pg_auth_members.member AND pg_auth_members.roleid={{ rid|qtLiteral }}::oid) pg + ) rolmembers + {% endif %} FROM pg_catalog.pg_roles r {% if rid %} diff --git a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/update.sql b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/update.sql index 388b7b83e..57323ae02 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.1_plus/update.sql @@ -124,3 +124,15 @@ GRANT {{ conn|qtIdent(data.members)|join(', ') }} TO {{ conn|qtIdent(rolname) }} COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }}; {% endif %} + +{% if 'rol_revoked_admins' in data and + data.rol_revoked_admins|length > 0 +%} + +REVOKE ADMIN OPTION FOR {{ conn|qtIdent(rolname) }} FROM {{ conn|qtIdent(data.rol_revoked_admins)|join(', ') }};{% endif %}{% if 'rol_revoked' in data and data.rol_revoked|length > 0 %} + +REVOKE {{ conn|qtIdent(rolname) }} FROM {{ conn|qtIdent(data.rol_revoked)|join(', ') }};{% endif %}{% if data.rol_admins and data.rol_admins|length > 0 %} + +GRANT {{ conn|qtIdent(rolname) }} TO {{ conn|qtIdent(data.rol_admins)|join(', ') }} WITH ADMIN OPTION;{% endif %}{% if data.rol_members and data.rol_members|length > 0 %} + +GRANT {{ conn|qtIdent(rolname) }} TO {{ conn|qtIdent(data.rol_members)|join(', ') }};{% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/properties.sql index 0f07798a6..8e7091b79 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/properties.sql +++ b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/properties.sql @@ -10,6 +10,15 @@ SELECT ORDER BY rm.rolname ) AS rolmembership, (SELECT pg_catalog.array_agg(provider || '=' || label) FROM pg_catalog.pg_shseclabel sl1 WHERE sl1.objoid=r.oid) AS seclabels + {% if rid %} + ,ARRAY( + SELECT + CASE WHEN pg.admin_option THEN '1' ELSE '0' END || pg.usename + FROM + (SELECT pg_roles.rolname AS usename, pg_auth_members.admin_option AS admin_option FROM pg_roles + JOIN pg_auth_members ON pg_roles.oid=pg_auth_members.member AND pg_auth_members.roleid={{ rid|qtLiteral }}::oid) pg + ) rolmembers + {% endif %} FROM pg_catalog.pg_roles r {% if rid %} diff --git a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/update.sql b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/update.sql index f77921f59..7a234ef1f 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/update.sql +++ b/web/pgadmin/browser/server_groups/servers/roles/templates/roles/sql/9.4_plus/update.sql @@ -99,3 +99,15 @@ GRANT {{ conn|qtIdent(data.members)|join(', ') }} TO {{ conn|qtIdent(rolname) }} COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }}; {% endif %} + +{% if 'rol_revoked_admins' in data and + data.rol_revoked_admins|length > 0 +%} + +REVOKE ADMIN OPTION FOR {{ conn|qtIdent(rolname) }} FROM {{ conn|qtIdent(data.rol_revoked_admins)|join(', ') }};{% endif %}{% if 'rol_revoked' in data and data.rol_revoked|length > 0 %} + +REVOKE {{ conn|qtIdent(rolname) }} FROM {{ conn|qtIdent(data.rol_revoked)|join(', ') }};{% endif %}{% if data.rol_admins and data.rol_admins|length > 0 %} + +GRANT {{ conn|qtIdent(rolname) }} TO {{ conn|qtIdent(data.rol_admins)|join(', ') }} WITH ADMIN OPTION;{% endif %}{% if data.rol_members and data.rol_members|length > 0 %} + +GRANT {{ conn|qtIdent(rolname) }} TO {{ conn|qtIdent(data.rol_members)|join(', ') }};{% endif %} diff --git a/web/pgadmin/static/scss/_select2.overrides.scss b/web/pgadmin/static/scss/_select2.overrides.scss index c17225b51..0f87c4034 100644 --- a/web/pgadmin/static/scss/_select2.overrides.scss +++ b/web/pgadmin/static/scss/_select2.overrides.scss @@ -117,7 +117,8 @@ select[readonly].select2-hidden-accessible + .select2-container { } select[readonly].select2-hidden-accessible + .select2-container .select2-selection { - background: #eee; + background: $select2-readonly; + color: $text-muted; box-shadow: none; } diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss index 1e357b312..ce33424e4 100644 --- a/web/pgadmin/static/scss/resources/_default.variables.scss +++ b/web/pgadmin/static/scss/resources/_default.variables.scss @@ -369,3 +369,5 @@ $erd-link-selected-color: $color-fg !default; @return '%23' + str-slice('#{$colour}', 2, -1) } $erd-bg-grid: url("data:image/svg+xml, %3Csvg width='100%25' viewBox='0 0 45 45' style='background-color:#{url-friendly-colour($erd-canvas-bg)}' height='100%25' xmlns='http://www.w3.org/2000/svg'%3E%3Cdefs%3E%3Cpattern id='smallGrid' width='15' height='15' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 15 0 L 0 0 0 15' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='0.5'/%3E%3C/pattern%3E%3Cpattern id='grid' width='45' height='45' patternUnits='userSpaceOnUse'%3E%3Crect width='100' height='100' fill='url(%23smallGrid)'/%3E%3Cpath d='M 100 0 L 0 0 0 100' fill='none' stroke='#{url-friendly-colour($erd-canvas-grid)}' stroke-width='1'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23grid)' /%3E%3C/svg%3E%0A"); + +$select2-readonly: $color-gray-lighter !default; diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss index 6912060bb..4605b0fcd 100644 --- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss @@ -134,3 +134,5 @@ $erd-canvas-bg: $color-gray-light; $erd-canvas-grid: #444952; $erd-link-color: $color-fg; $erd-link-selected-color: $color-fg; + +$select2-readonly: $color-bg; diff --git a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss index 2d26af1fe..65d80cbc3 100644 --- a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss +++ b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss @@ -205,3 +205,5 @@ $span-text-color: #9D9FA1 !default; $span-text-color-hover: $black !default; $quick-search-a-text-color: $black !default; $quick-search-info-icon: #8A8A8A !default; + +$select2-readonly: $color-gray;