Show the login roles that are members of a group role be shown when examining a group role. Fixes #5404

This commit is contained in:
Nikhil Mohite 2021-03-12 13:11:09 +05:30 committed by Akshay Joshi
parent 68132e2a8f
commit 7bad4ca535
13 changed files with 209 additions and 15 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -9,6 +9,7 @@ This release contains a number of bug fixes and new features since the release o
New features New features
************ ************
| `Issue #5404 <https://redmine.postgresql.org/issues/5404>`_ - Show the login roles that are members of a group role be shown when examining a group role.
| `Issue #6268 <https://redmine.postgresql.org/issues/6268>`_ - Make 'kerberos' an optional feature in the Python wheel, to avoid the need to install MIT Kerberos on the system by default. | `Issue #6268 <https://redmine.postgresql.org/issues/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 <https://redmine.postgresql.org/issues/6270>`_ - Added '--replace' option in Import server to replace the list of servers with the newly imported one. | `Issue #6270 <https://redmine.postgresql.org/issues/6270>`_ - Added '--replace' option in Import server to replace the list of servers with the newly imported one.

View File

@ -71,11 +71,11 @@ Use the *Privileges* tab to grant privileges to the role.
:alt: Role dialog membership tab :alt: Role dialog membership tab
:align: center :align: center
* Specify members of the role in the *Role Membership* field. Click inside the * Specify member of the role in the *Member of* field and specify the members in the *Member* field.
*Roles* field to select role names from a drop down list. Confirm each Confirm each selection by checking the checkbox to the right of the role name;
selection by checking the checkbox to the right of the role name; delete a delete a selection by clicking the *x* to the left of the role name.
selection by clicking the *x* to the left of the role name. Membership conveys Membership conveys the privileges granted to the specified role to each of
the privileges granted to the specified role to each of its members. its members.
Click the *Parameters* tab to continue. Click the *Parameters* tab to continue.

View File

@ -236,6 +236,105 @@ class RoleView(PGChildNodeView):
data['rolmembership'].get('deleted', []), data['rolmembership'].get('deleted', []),
lambda _: True, 'role') 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): def _validate_rolemembership(self, id, data):
""" """
Validate the rolmembership data dict Validate the rolmembership data dict
@ -433,7 +532,7 @@ rolmembership:{
'rolcanlogin', 'rolsuper', 'rolcreatedb', 'rolcanlogin', 'rolsuper', 'rolcreatedb',
'rolcreaterole', 'rolinherit', 'rolreplication', 'rolcreaterole', 'rolinherit', 'rolreplication',
'rolcatupdate', 'variables', 'rolmembership', 'rolcatupdate', 'variables', 'rolmembership',
'seclabels' 'seclabels', 'rolmembers'
]: ]:
data[key] = json.loads(val, encoding='utf-8') data[key] = json.loads(val, encoding='utf-8')
else: else:
@ -466,6 +565,11 @@ rolmembership:{
if invalid_msg is not None: if invalid_msg is not None:
return precondition_required(invalid_msg) 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 self.request = data
return f(self, **kwargs) return f(self, **kwargs)
@ -716,6 +820,16 @@ rolmembership:{
}) })
row['seclabels'] = res 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') @check_precondition(action='properties')
def properties(self, gid, sid, rid): def properties(self, gid, sid, rid):

View File

@ -90,7 +90,7 @@ define('pgadmin.node.role', [
template: _.template([ template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>', '<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<div class="<%=Backform.controlsClassName%>">', '<div class="<%=Backform.controlsClassName%>">',
' <select title = <%=label%> multiple="multiple" style="width:100%;" class="pgadmin-controls <%=extraClasses.join(\' \')%>" name="<%=name%>" value="<%-JSON.stringify(value)%>" <%=disabled ? "disabled" : ""%> <%=required ? "required" : ""%>>', ' <select title = <%=label%> 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++) { %>', ' <% for (var i=0; i < options.length; i++) { %>',
' <% var option = options[i]; %>', ' <% var option = options[i]; %>',
' <option value=<%-option.value%> data-icon=<%-option.image%> <%=value != null && _.indexOf(value, option.value) != -1 ? "selected" : ""%> <%=option.disabled ? "disabled=\'disabled\'" : ""%>><%-option.label%></option>', ' <option value=<%-option.value%> data-icon=<%-option.image%> <%=value != null && _.indexOf(value, option.value) != -1 ? "selected" : ""%> <%=option.disabled ? "disabled=\'disabled\'" : ""%>><%-option.label%></option>',
@ -107,7 +107,7 @@ define('pgadmin.node.role', [
' <span><%= opttext %><span>', ' <span><%= opttext %><span>',
' <% if (checkbox) { %>', ' <% if (checkbox) { %>',
' <div class="custom-control custom-checkbox custom-checkbox-no-label d-inline">', ' <div class="custom-control custom-checkbox custom-checkbox-no-label d-inline">',
' <input tabindex="-1" type="checkbox" class="custom-control-input" id="check_<%= opttext %>" />', ' <input tabindex="-1" type="checkbox" class="custom-control-input" id="check_<%= opttext %>" <%=disabled ? "disabled" : ""%> />',
' <label class="custom-control-label" for="check_<%= opttext %>">', ' <label class="custom-control-label" for="check_<%= opttext %>">',
' <span class="sr-only">WITH ADMIN<span>', ' <span class="sr-only">WITH ADMIN<span>',
' </label>', ' </label>',
@ -149,6 +149,7 @@ define('pgadmin.node.role', [
_.extend(data, { _.extend(data, {
disabled: evalF(data.disabled, data, this.model), disabled: evalF(data.disabled, data, this.model),
visible: evalF(data.visible, data, this.model), visible: evalF(data.visible, data, this.model),
readonly: evalF(data.readonly, data, this.model),
required: evalF(data.required, data, this.model), required: evalF(data.required, data, this.model),
helpMessage: evalASFunc(data.helpMessage, data, this.model), helpMessage: evalASFunc(data.helpMessage, data, this.model),
}); });
@ -242,7 +243,7 @@ define('pgadmin.node.role', [
multiple: true, multiple: true,
tags: true, tags: true,
allowClear: data.disabled ? false : true, allowClear: data.disabled ? false : true,
placeholder: data.disabled ? '' : gettext('Select members'), placeholder: data.disabled ? '' : gettext('Select roles'),
width: 'style', width: 'style',
}).on('change', function(e) { }).on('change', function(e) {
$(e.target).find(':selected').each(function() { $(e.target).find(':selected').each(function() {
@ -386,6 +387,7 @@ define('pgadmin.node.role', [
rolcatupdate: false, rolcatupdate: false,
rolreplication: false, rolreplication: false,
rolmembership: [], rolmembership: [],
rolmembers: [],
rolvaliduntil: null, rolvaliduntil: null,
seclabels: [], seclabels: [],
variables: [], variables: [],
@ -492,11 +494,12 @@ define('pgadmin.node.role', [
controlsClassName: 'pgadmin-controls pg-el-sm-8 pg-el-12', controlsClassName: 'pgadmin-controls pg-el-sm-8 pg-el-12',
min_version: 90100, min_version: 90100,
readonly: 'readonly', readonly: 'readonly',
},{ },
id: 'rolmembership', label: gettext('Roles'), {
id: 'rolmembership', label: gettext('Member of'),
group: gettext('Membership'), type: 'collection', group: gettext('Membership'), type: 'collection',
cell: 'string', readonly: 'readonly', cell: 'string', mode: ['properties', 'edit', 'create'],
mode: ['properties', 'edit', 'create'], readonly: 'readonly',
control: RoleMembersControl, model: pgBrowser.Node.Model.extend({ control: RoleMembersControl, model: pgBrowser.Node.Model.extend({
keys: ['role'], keys: ['role'],
idAttribute: '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.'); 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', id: 'variables', label: '', type: 'collection',
group: gettext('Parameters'), hasDatabase: true, url: 'variables', group: gettext('Parameters'), hasDatabase: true, url: 'variables',
model: pgBrowser.Node.VariableModel.extend({keys:['name', 'database']}), model: pgBrowser.Node.VariableModel.extend({keys:['name', 'database']}),

View File

@ -9,6 +9,15 @@ SELECT
LEFT JOIN pg_catalog.pg_roles rm ON (rm.oid = am.roleid) LEFT JOIN pg_catalog.pg_roles rm ON (rm.oid = am.roleid)
) rolmembership, ) rolmembership,
(SELECT pg_catalog.array_agg(provider || '=' || label) FROM pg_catalog.pg_shseclabel sl1 WHERE sl1.objoid=r.oid) AS seclabels (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 FROM
pg_catalog.pg_roles r pg_catalog.pg_roles r
{% if rid %} {% if rid %}

View File

@ -124,3 +124,15 @@ GRANT {{ conn|qtIdent(data.members)|join(', ') }} TO {{ conn|qtIdent(rolname) }}
COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }}; COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }};
{% endif %} {% 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 %}

View File

@ -10,6 +10,15 @@ SELECT
ORDER BY rm.rolname ORDER BY rm.rolname
) AS rolmembership, ) AS rolmembership,
(SELECT pg_catalog.array_agg(provider || '=' || label) FROM pg_catalog.pg_shseclabel sl1 WHERE sl1.objoid=r.oid) AS seclabels (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 FROM
pg_catalog.pg_roles r pg_catalog.pg_roles r
{% if rid %} {% if rid %}

View File

@ -99,3 +99,15 @@ GRANT {{ conn|qtIdent(data.members)|join(', ') }} TO {{ conn|qtIdent(rolname) }}
COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }}; COMMENT ON ROLE {{ conn|qtIdent(rolname) }} IS {{ data.description|qtLiteral }};
{% endif %} {% 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 %}

View File

@ -117,7 +117,8 @@ select[readonly].select2-hidden-accessible + .select2-container {
} }
select[readonly].select2-hidden-accessible + .select2-container .select2-selection { select[readonly].select2-hidden-accessible + .select2-container .select2-selection {
background: #eee; background: $select2-readonly;
color: $text-muted;
box-shadow: none; box-shadow: none;
} }

View File

@ -369,3 +369,5 @@ $erd-link-selected-color: $color-fg !default;
@return '%23' + str-slice('#{$colour}', 2, -1) @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"); $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;

View File

@ -134,3 +134,5 @@ $erd-canvas-bg: $color-gray-light;
$erd-canvas-grid: #444952; $erd-canvas-grid: #444952;
$erd-link-color: $color-fg; $erd-link-color: $color-fg;
$erd-link-selected-color: $color-fg; $erd-link-selected-color: $color-fg;
$select2-readonly: $color-bg;

View File

@ -205,3 +205,5 @@ $span-text-color: #9D9FA1 !default;
$span-text-color-hover: $black !default; $span-text-color-hover: $black !default;
$quick-search-a-text-color: $black !default; $quick-search-a-text-color: $black !default;
$quick-search-info-icon: #8A8A8A !default; $quick-search-info-icon: #8A8A8A !default;
$select2-readonly: $color-gray;