Files
pgadmin4/web/pgadmin/browser/server_groups/servers/static/js/privilege.js

764 lines
25 KiB
JavaScript

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
define(['sources/gettext', 'underscore', 'jquery', 'backbone', 'backform',
'backgrid', 'alertify', 'pgadmin.browser.node', 'sources/utils', 'pgadmin.browser.node.ui',
], function(gettext, _, $, Backbone, Backform, Backgrid, Alertify, pgNode, commonUtils) {
/**
* Each Privilege, supporeted by an database object, will be represented
* using this Model.
*
* Defaults:
* privilege_type -> Name of the permission
* i.e. CREATE, TEMPORARY, CONNECT, etc.
* privilege -> Has privilege? (true/false)
* with_grant -> Has privilege with grant option (true/false)
**/
var PrivilegeModel = pgNode.Model.extend({
idAttribute: 'privilege_type',
defaults: {
privilege_type: undefined,
privilege: false,
with_grant: false,
},
validate: function() {
return null;
},
});
/**
* A database object has privileges item list (aclitem[]).
*
* This model represents the individual privilege item (aclitem).
* It has basically three properties:
* + grantee - Role to which that privilege applies to.
* Empty value represents to PUBLIC.
* + grantor - Grantor who has given this permission.
* + privileges - Privileges for that role.
**/
var PrivilegeRoleModel = pgNode.PrivilegeRoleModel = pgNode.Model.extend({
idAttribute: 'grantee',
defaults: {
grantee: undefined,
grantor: undefined,
privileges: undefined,
},
keys: ['grantee', 'grantor'],
/*
* Each of the database object needs to extend this model, which should
* provide the type of privileges (it supports).
*/
privileges:[],
schema: [{
id: 'grantee', label: gettext('Grantee'), type:'text', group: null,
editable: true, cellHeaderClasses: 'width_percent_40',
node: 'role', options_cached: false,
disabled : function(m) {
if (!(m instanceof Backbone.Model)) {
// This has been called during generating the header cell
return false;
}
return !(
m.top && m.top.node_info &&
m.top.node_info.server.user.name == m.get('grantor')
);
},
transform: function() {
var res =
Backgrid.Extension.NodeListByNameCell.prototype.defaults.transform.apply(
this, arguments
);
res.unshift({label: 'PUBLIC', value: 'PUBLIC'});
return res;
},
cell: Backgrid.Extension.NodeListByNameCell.extend({
initialize: function(opts) {
var self = this,
override_opts = true;
// We would like to override the original options, because - we
// should omit the existing role/user from the privilege cell.
// Because - the column is shared among all the cell, we can only
// override only once.
if (opts && opts.column &&
opts.column instanceof Backbone.Model &&
opts.column.get('options_cached')) {
override_opts = false;
}
Backgrid.Extension.NodeListByNameCell.prototype.initialize.apply(
self, arguments
);
// Let's override the options
if (override_opts) {
opts = self.column.get('options');
self.column.set(
'options', self.omit_selected_roles.bind(self, opts)
);
}
var rerender = function (m) {
var _self = this;
if ('grantee' in m.changed && this.model.cid != m.cid) {
setTimeout(
function() {
_self.render();
}, 50
);
}
}.bind(this);
// We would like to rerender all the cells of this type for this
// collection, because - we need to omit the newly selected roles
// form the list. Also, the render will be automatically called for
// the model represented by this cell, we will not do that again.
this.listenTo(self.model.collection, 'change', rerender, this);
this.listenTo(self.model.collection, 'remove', rerender, this);
},
// Remove all the selected roles (though- not mine).
omit_selected_roles: function(opts, cell) {
var res = opts(cell),
selected = {},
model = cell.model,
cid = model.cid,
// We need to check node_info values in parent when object is nested.
// eg: column level privileges in table dialog
// In this case node_info will not be avilable to column node as
// it is not loaded yet
node_info = (_.has(model.top, 'node_info')
&& !_.isUndefined(model.top.node_info)) ?
model.top.node_info :
model.handler.top.node_info,
curr_user = node_info.server.user.name;
model.collection.each(function(m) {
var grantee = m.get('grantee');
if (m.cid != cid && !_.isUndefined(grantee) &&
curr_user == m.get('grantor')) {
selected[grantee] = m.cid;
}
});
res = _.filter(res, function(o) {
return !(o.value in selected);
});
return res;
},
}),
},{
id: 'privileges', label: gettext('Privileges'),
type: 'collection', model: PrivilegeModel, group: null,
cell: 'privilege', control: 'text', cellHeaderClasses: 'width_percent_40',
disabled : function(column) {
if (column instanceof Backbone.Collection) {
// This has been called during generating the header cell
return false;
}
return !(
this.node_info &&
this.node_info.server.user.name == column.get('grantor') ||
this.attributes.node_info.server.user.name == column.get('grantor')
);
},
},{
id: 'grantor', label: gettext('Grantor'), type: 'text', readonly: true,
cell: 'node-list-by-name', node: 'role',
}],
/*
* Initialize the model, which will transform the privileges string to
* collection of Privilege Model.
*/
initialize: function(attrs, opts) {
pgNode.Model.prototype.initialize.apply(this, arguments);
if (_.isNull(attrs)) {
this.set(
'grantor',
opts && opts.top && opts.top.node_info && opts.top.node_info.server.user.name,
{silent: true}
);
}
/*
* Define the collection of the privilege supported by this model
*/
var self = this,
models = self.get('privileges'),
privileges = this.get('privileges') || {};
if (_.isArray(privileges)) {
privileges = new (pgNode.Collection)(
models, {
model: PrivilegeModel,
top: this.top || this,
handler: this,
silent: true,
parse: false,
});
this.set('privileges', privileges, {silent: true});
}
var privs = {};
_.each(self.privileges, function(p) {
privs[p] = {
'privilege_type': p, 'privilege': false, 'with_grant': false,
};
});
privileges.each(function(m) {
delete privs[m.get('privilege_type')];
});
_.each(privs, function(p) {
privileges.add(p, {silent: true});
});
self.on('change:grantee', self.granteeChanged);
privileges.on('change', function() {
self.trigger('change:privileges', self);
});
return self;
},
granteeChanged: function() {
var privileges = this.get('privileges'),
grantee = this.get('grantee');
// Reset all with grant options if grantee is public.
if (grantee == 'PUBLIC') {
privileges.each(function(m) {
m.set('with_grant', false, {silent: true});
});
}
},
toJSON: function() {
var privileges = [];
if (this.attributes &&
!this.attributes['privileges']) {
return null;
}
this.attributes['privileges'].each(
function(p) {
if (p.get('privilege')) {
privileges.push(p.toJSON());
}
});
return {
'grantee': this.get('grantee'),
'grantor': this.get('grantor'),
'privileges': privileges,
};
},
validate: function() {
var errmsg = null,
msg;
if (_.isUndefined(this.get('grantee'))) {
msg = gettext('A grantee must be selected.');
this.errorModel.set('grantee', msg);
errmsg = msg;
} else {
this.errorModel.unset('grantee');
}
if (this.attributes &&
this.attributes['privileges']) {
var anyPrivSelected = false;
this.attributes['privileges'].each(
function(p) {
if (p.get('privilege')) {
anyPrivSelected = true;
}
});
if (!anyPrivSelected) {
msg = gettext('At least one privilege should be selected.');
this.errorModel.set('privileges', msg);
errmsg = errmsg || msg;
} else {
this.errorModel.unset('privileges');
}
}
return errmsg;
},
});
/**
Custom cell editor for editing privileges.
*/
var PrivilegeCellEditor = Backgrid.Extension.PrivilegeCellEditor =
Backgrid.CellEditor.extend({
tagName: 'div',
// All available privileges in the PostgreSQL database server for
// generating the label for the specific Control
Labels: {
'C': 'CREATE',
'T': 'TEMPORARY',
'c': 'CONNECT',
'a': 'INSERT',
'r': 'SELECT',
'w': 'UPDATE',
'd': 'DELETE',
'D': 'TRUNCATE',
'x': 'REFERENCES',
't': 'TRIGGER',
'U': 'USAGE',
'X': 'EXECUTE',
},
template: _.template([
'<tr class="<%= header ? "header" : "" %>">',
' <td class="renderable">',
' <div class="custom-control custom-checkbox privilege-checkbox">',
' <input tabindex="0" type="checkbox" class="custom-control-input" id="<%= checkbox_id %>" name="privilege" privilege="<%- privilege_type %>" target="<%- target %>" <%= privilege ? \'checked\' : "" %>/>',
' <label class="custom-control-label" for="<%= checkbox_id %>">',
' <%- privilege_label %>',
' </label>',
' </div>',
' </td>',
' <td class="renderable">',
' <div class="custom-control custom-checkbox privilege-checkbox">',
' <input tabindex="0" type="checkbox" class="custom-control-input" id="wgo_<%= checkbox_id %>" name="with_grant" privilege="<%- privilege_type %>" target="<%- target %>" <%= with_grant ? \'checked\' : "" %> <%= enable_with_grant ? "" : \'disabled\'%>/>',
' <label class="custom-control-label" for="wgo_<%= checkbox_id %>">',
' WITH GRANT OPTION',
' </label>',
' </div>',
' </td>',
'</tr>'].join(' '), null, {variable: null}),
events: {
'change': 'privilegeChanged',
'blur': 'lostFocus',
'keydown': 'lostFocus',
},
render: function () {
this.$el.empty();
this.$el.attr('tabindex', '1');
this.$el.attr('target', this.elId);
var collection = this.model.get(this.column.get('name')),
tbl = $('<table aria-label='+this.column.get('label')+'></table>').appendTo(this.$el),
self = this,
privilege = true, with_grant = true;
// For each privilege generate html template.
// List down all the Privilege model.
var checkbox_id = _.uniqueId();
collection.each(function(m) {
var d = m.toJSON();
privilege = (privilege && d.privilege);
with_grant = (with_grant && privilege && d.with_grant);
_.extend(
d, {
'target': self.cid,
'header': false,
'privilege_label': self.Labels[d.privilege_type],
'with_grant': (self.model.get('grantee') != 'PUBLIC' && d.with_grant),
'enable_with_grant': (self.model.get('grantee') != 'PUBLIC' && d.privilege),
'checkbox_id': d.privilege_type + '' + checkbox_id,
});
privilege = (privilege && d.privilege);
with_grant = (with_grant && privilege && d.with_grant);
tbl.append(self.template(d));
});
if (collection.length > 1) {
// Preprend the ALL controls on that table
tbl.prepend(
self.template({
'target': self.cid,
'privilege_label': 'ALL',
'privilege_type': 'ALL',
'privilege': privilege,
'with_grant': (self.model.get('grantee') != 'PUBLIC' && with_grant),
'enable_with_grant': (self.model.get('grantee') != 'PUBLIC' && privilege),
'header': true,
'checkbox_id': 'all' + '' + checkbox_id,
}));
}
self.$el.find('input[type=checkbox]').first().trigger('focus');
// Since blur event does not bubble we need to explicitly call parent's blur event.
$(self.$el.find('input[type=checkbox]')).on('blur',function() {
self.$el.trigger('blur');
});
// Make row visible in when entering in edit mode.
$(self.$el).pgMakeVisible('backform-tab');
self.delegateEvents();
return this;
},
/*
* Listen to the checkbox value change and update the model accordingly.
*/
privilegeChanged: function(ev) {
if (ev && ev.target) {
/*
* We're looking for checkboxes only.
*/
var $el = $(ev.target),
privilege_type = $el.attr('privilege'),
type = $el.attr('name'),
checked = $el.prop('checked'),
$tr = $el.closest('tr'),
$tbl = $tr.closest('table'),
collection = this.model.get('privileges'),
grantee = this.model.get('grantee'), $allGrants,
$allPrivileges, $elGrant;
this.undelegateEvents();
/*
* If the checkbox selected/deselected is for 'ALL', we will select all
* the checkbox for each privilege.
*/
if (privilege_type == 'ALL') {
var allPrivilege, allWithGrant;
$elGrant = $tr.find('input[name=with_grant]');
$allPrivileges = $tbl.find(
'input[name=privilege][privilege!=\'ALL\']'
);
$allGrants = $tbl.find(
'input[name=with_grant][privilege!=\'ALL\']'
);
if (type == 'privilege') {
/*
* We clicked the privilege checkbox, and not checkbox for with
* grant options.
*/
allPrivilege = checked;
allWithGrant = false;
if (checked) {
$allPrivileges.prop('checked', true);
/*
* We have clicked the ALL checkbox, we should be able to select
* the grant options too.
*/
if (grantee == 'PUBLIC') {
$allGrants.prop('disabled', true);
$elGrant.prop('disabled', true);
} else {
$allGrants.prop('disabled', false);
$elGrant.prop('disabled', false);
}
} else {
/*
* ALL checkbox has been deselected, hence - we need to make
* sure.
* 1. Deselect all the privileges checkboxes
* 2. Deselect and disable all with grant privilege checkboxes.
* 3. Deselect and disable the checkbox for ALL with grant privilege.
*/
$allPrivileges.prop('checked', false);
$elGrant.prop('checked', false);
$allGrants.prop('checked', false);
$elGrant.prop('disabled', true);
$allGrants.prop('disabled', true);
}
} else {
/*
* We were able to click the ALL with grant privilege checkbox,
* that means, privilege for Privileges are true.
*
* We need to select/deselect all the with grant options
* checkboxes, based on the current value of the ALL with grant
* privilege checkbox.
*/
allPrivilege = true;
allWithGrant = checked;
$allGrants.prop('checked', checked);
}
/*
* Set the values for each Privilege Model.
*/
collection.each(function(m) {
m.set(
{'privilege': allPrivilege, 'with_grant': allWithGrant}
);
});
} else {
/*
* Particular privilege has been selected/deselected, which can be
* identified using the privilege="X" attribute.
*/
var attrs = {};
$tbl = $tr.closest('table');
$allPrivileges = $tbl.find(
'input[name=privilege][privilege=\'ALL\']'
);
$allGrants = $tbl.find(
'input[name=with_grant][privilege=\'ALL\']'
);
attrs[type] = checked;
if (type == 'privilege') {
$elGrant = ($el.closest('tr')).find('input[name=with_grant]');
if (!checked) {
attrs['with_grant'] = false;
$elGrant.prop('checked', false).prop('disabled', true);
$allPrivileges.prop('checked', false);
$allGrants.prop('disabled', true);
$allGrants.prop('checked', false);
} else if (grantee != 'PUBLIC') {
$elGrant.prop('disabled', false);
}
} else if (!checked) {
$allGrants.prop('checked', false);
}
collection.get(privilege_type).set(attrs);
if (checked) {
$allPrivileges = $tbl.find(
'input[name=privilege][privilege!=\'ALL\']:checked'
);
if ($allPrivileges.length > 1 &&
$allPrivileges.length == collection.models.length) {
$allPrivileges.prop('checked', true);
if (type == 'with_grant') {
$allGrants = $tbl.find(
'input[name=with_grant][privilege!=\'ALL\']:checked'
);
if ($allGrants.length == collection.models.length) {
$allGrants.prop('disabled', false);
$allGrants.prop('checked', true);
}
} else if (grantee != 'PUBLIC') {
$allGrants.prop('disabled', false);
}
}
}
}
this.model.trigger('change', this.model);
var anySelected = false,
msg = null;
collection.each(function(m) {
anySelected = anySelected || m.get('privilege');
});
if (anySelected) {
this.model.errorModel.unset('privileges');
if (this.model.errorModel.has('grantee')) {
msg = this.model.errorModel.get('grantee');
}
} else {
this.model.errorModel.set(
'privileges', gettext('At least one privilege should be selected.')
);
msg = gettext('At least one privilege should be selected.');
}
if (msg) {
this.model.collection.trigger(
'pgadmin-session:model:invalid', msg, this.model
);
} else {
this.model.collection.trigger(
'pgadmin-session:model:valid', this.model
);
}
}
this.delegateEvents();
},
lostFocus: function(ev) {
/*
* We lost the focus, it's time for us to exit the editor.
*/
var self = this,
/*
* Function to determine whether one dom element is descendant of another
* dom element.
*/
isDescendant = function (parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
},
model = this.model,
column = this.column,
command = new Backgrid.Command(ev),
coll = this.model.get(this.column.get('name'));
if (ev.key == 'Tab'){
commonUtils.handleKeyNavigation(event);
}
if (command.moveUp() || command.moveDown() || command.save() || command.cancel() ||
(command.moveLeft() && ev.target.name === 'privilege' && $(ev.target).attr('privilege') === 'ALL')) {
// undo
ev.stopPropagation();
model.trigger('backgrid:edited', model, column, command);
return;
} else if (command.moveRight()) {
// If we are at the last privilege then we should move to next cell
if (coll.last().get('privilege_type') === $(ev.target).attr('privilege')) {
if ((ev.target.name === 'privilege' && !ev.target.checked ) ||
$(ev.target).attr('name') === 'with_grant') {
ev.stopPropagation();
model.trigger('backgrid:edited', model, column, command);
return;
}
}
}
/*
* Between leaving the old element focus and entering the new element focus the
* active element is the document/body itself so add timeout to get the proper
* focused active element.
*/
setTimeout(function() {
/*
* Do not close the control if user clicks outside dialog window,
* only close the row if user clicks on add button or on another row,
* if user clicks somewhere else then we will get tagName as 'BODY'
* or 'WINDOW'
*/
var is_active_element = document.activeElement.tagName == 'DIV' ||
document.activeElement.tagName == 'BUTTON';
if (is_active_element && self.$el[0] != document.activeElement &&
!isDescendant(self.$el[0], document.activeElement)) {
var m = self.model;
m.trigger('backgrid:edited', m, self.column, new Backgrid.Command(ev));
}},10);
},
});
/*
* This will help us transform the privileges value in proper format to be
* displayed in the cell.
*/
var PrivilegeCellFormatter = Backgrid.Extension.PrivilegeCellFormatter =
function () {};
_.extend(PrivilegeCellFormatter.prototype, {
notation: {
'CREATE' : 'C',
'TEMPORARY' : 'T',
'CONNECT' : 'c',
'INSERT' : 'a',
'SELECT' : 'r',
'UPDATE' : 'w',
'DELETE' : 'd',
'TRUNCATE' : 'D',
'REFERENCES' : 'x',
'TRIGGER' : 't',
'USAGE' : 'U',
'EXECUTE' : 'X',
},
/**
* Takes a raw value from a model and returns an optionally formatted
* string for display.
*/
fromRaw: function (rawData) {
var res = '';
if (rawData instanceof Backbone.Collection) {
rawData.each(function(m) {
if (m.get('privilege')) {
res += m.get('privilege_type');
if (m.get('with_grant')) {
res += '*';
}
}
});
}
return res;
},
});
/*
* PrivilegeCell for rendering and taking input for the privileges.
*/
Backgrid.Extension.PrivilegeCell = Backgrid.Cell.extend({
className: 'edit-cell',
formatter: PrivilegeCellFormatter,
editor: PrivilegeCellEditor,
initialize: function () {
var self = this;
Backgrid.Cell.prototype.initialize.apply(this, arguments);
self.model.on('change:grantee', function() {
if (!self.$el.hasClass('editor')) {
/*
* Add time out before render; As we might want to wait till model
* is updated by PrivilegeRoleModel:granteeChanged.
*/
setTimeout(function() {
self.render();
},10);
}
});
},
events: {
'click': 'enterEditMode',
'keydown': 'saveOrCancel',
},
saveOrCancel: function (e) {
var model = this.model;
var column = this.column;
var command = new Backgrid.Command(e);
if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() ||
command.save()) {
e.preventDefault();
e.stopPropagation();
model.trigger('backgrid:edited', model, column, command);
}
// esc
else if (command.cancel()) {
// undo
e.stopPropagation();
model.trigger('backgrid:edited', model, column, command);
}
},
});
return PrivilegeRoleModel;
});