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
************
| `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 #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
: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.

View File

@ -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):

View File

@ -90,7 +90,7 @@ define('pgadmin.node.role', [
template: _.template([
'<label class="<%=Backform.controlLabelClassName%>"><%=label%></label>',
'<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++) { %>',
' <% 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>',
@ -107,7 +107,7 @@ define('pgadmin.node.role', [
' <span><%= opttext %><span>',
' <% if (checkbox) { %>',
' <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 %>">',
' <span class="sr-only">WITH ADMIN<span>',
' </label>',
@ -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']}),

View File

@ -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 %}

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 }};
{% 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
) 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 %}

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 }};
{% 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 {
background: #eee;
background: $select2-readonly;
color: $text-muted;
box-shadow: none;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;