diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 38693d4..e00b6a7 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -153,7 +153,8 @@ class BrowserModule(PgAdminModule): for name, end in [ ['pgadmin.browser.menu', 'js/menu'], ['pgadmin.browser.panel', 'js/panel'], - ['pgadmin.browser.frame', 'js/frame']]: + ['pgadmin.browser.frame', 'js/frame'], + ['pgadmin.browser.wizard', 'js/wizard']]: scripts.append({ 'name': name, 'path': url_for('browser.static', filename=end), 'preloaded': True}) diff --git a/web/pgadmin/browser/static/js/node.ui.js b/web/pgadmin/browser/static/js/node.ui.js index b84b6ee..47f1266 100644 --- a/web/pgadmin/browser/static/js/node.ui.js +++ b/web/pgadmin/browser/static/js/node.ui.js @@ -279,7 +279,7 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) { return false; }; - while(p) { + while(p && p.length > 0) { top = p.get(0).offsetTop + p.height(); p = p.parent(); if (hasScrollbar(p)) { diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index 4346156..8f913d2 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -108,7 +108,7 @@ return m[idx > len ? 0 : idx]; } return type; - } + }; var BackformControlInit = Backform.Control.prototype.initialize, @@ -745,6 +745,13 @@ self.stopListening(self.collection, "change", self.collectionChanged); } + // Remove grid + if (this.grid) { + this.grid.remove(); + delete this.grid; + this.grid = null; + } + Backform.Control.prototype.remove.apply(this, arguments); }, collectionChanged: function(newModel, coll, op) { @@ -1146,8 +1153,12 @@ /* * We will listen to the tab change event to check, if the SQL tab has * been clicked or, not. + * + * Also, listen to the wizard next/prev page changed */ this.model.on('pg-property-tab-changed', this.onTabChange, this); + this.model.on('pgadmin-wizard:nextpage:sql', this.onWizardNextPageChange, this); + this.model.on('pgadmin-wizard:prevpage:sql', this.onWizardPrevPageChange, this); }, getValueFromDOM: function() { return this.formatter.toRaw(this.$el.find("textarea").val(), this.model); @@ -1205,8 +1216,47 @@ } } }, + + // This method fetches the modified SQL for the wizard + onWizardNextPageChange: function(obj){ + + var self = this, + m = self.model, + gid = m.node_info['server-group']._id, + sid = m.node_info.server._id, + did = m.node_info.database._id, + nspname = m.node_info.schema.label; + + // generate encoded url based on wizard type + msql_url = "/wizard/"+obj.wizard_type+"/msql/"+ + S('%s/%s/%s/%s/%s/').sprintf( + encodeURI(gid), encodeURI(sid), encodeURI(did), + encodeURI(obj.node_type), encodeURI(nspname)).value(); + + // Fetches modified SQL + $.ajax({ + url: msql_url, + type: 'GET', + cache: false, + data: self.model.toJSON(true, 'GET'), + dataType: "json", + contentType: "application/json" + }).done(function(res) { + self.sqlTab.clearHistory(); + self.sqlTab.setValue(res.data); + }).fail(function() { + self.model.trigger('pgadmin-view:msql:error'); + }).always(function() { + self.model.trigger('pgadmin-view:msql:fetched'); + }); + + }, remove: function() { this.model.off('pg-property-tab-changed', this.onTabChange, this); + + // Stop listen to next/prev page events + this.model.off('pgadmin-wizard:nextpage:sql', this.onWizardNextPageChange, this); + this.model.off('pgadmin-wizard:prevpage:sql', this.onWizardPrevPageChange, this); Backform.Control.__super__.remove.apply(this, arguments); } }); diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html index 6ea5d69..d91203a 100755 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -28,6 +28,8 @@ + + {% for stylesheet in current_app.stylesheets %} diff --git a/web/pgadmin/tools/grantwizard/__init__.py b/web/pgadmin/tools/grantwizard/__init__.py new file mode 100644 index 0000000..8b2e82b --- /dev/null +++ b/web/pgadmin/tools/grantwizard/__init__.py @@ -0,0 +1,286 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2016, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +""" Implements Grant Wizard""" + +import json +from flask import render_template, request, current_app +from flask.ext.babel import gettext +from pgadmin.utils.ajax import make_response as ajax_response, \ + make_json_response, internal_server_error +from pgadmin.utils.driver import get_driver +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.server_groups.servers.utils import parse_priv_to_db +from pgadmin.utils import PgAdminModule +from flask import Response, url_for +from flask.ext.security import login_required + +# As unicode type is not available in python3 +# If we check a variable is "isinstance(variable, str) +# it breaks in python 3 as variable type is not string its unicode. +# We assign basestring as str type if it is python3, unicode +# if it is python2. + +try: + unicode = unicode +except NameError: + # 'unicode' is undefined, must be Python 3 + str = str + unicode = str + bytes = bytes + basestring = (str, bytes) +else: + # 'unicode' exists, must be Python 2 + str = str + unicode = unicode + bytes = str + basestring = basestring + +MODULE_NAME = 'grantwizard' + + +class GrantWizardModule(PgAdminModule): + """ + class GrantWizardModule(Object): + + It is a wizard which inherits PgAdminModule + class and define methods to load its own + javascript file. + """ + def get_own_javascripts(self): + """Add grantwizard.js file to load it on window load""" + return [{ + 'name': 'pgadmin.grantwizard', + 'path': url_for('grantwizard.index') + 'grantwizard', + 'when': None + } + ] + +# Create blueprint for GrantWizardModule class +blueprint = GrantWizardModule( + MODULE_NAME, __name__, static_url_path='', + url_prefix='/wizard') + + +@blueprint.route("/") +@login_required +def index(): + pass + + +@blueprint.route("/grantwizard.js") +@login_required +def script(): + """render own javascript""" + return Response(response=render_template( + "grantwizard/js/grantwizard.js", _=gettext), + status=200, + mimetype="application/javascript") + + +@blueprint.route( + '/grantwizard/properties///' + '/////', + methods=('GET', 'POST')) +@login_required +def properties(gid, sid, did, node_id, node_name, node_type, nspname): + """It fetches the properties of object types + and render into selection page of wizard + """ + res_data = [] + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection(did=did) + + # we will set template path for sql scripts + template_path = 'grantwizard/sql' + + # Fetch functions against schema + if node_type in ['schema']: + SQL = render_template("/".join( + [template_path, 'properties.sql']), + node_id=node_id, node_name=node_name, + type=node_type, nspname=nspname) + + status, res = conn.execute_dict(SQL) + + if not status: + return internal_server_error(errormsg=res) + + res_data.extend(res['rows']) + + # Fetch trigger functions + if node_type in ['schema', 'trigger_function']: + SQL = render_template("/".join( + [template_path, 'properties.sql']), + node_id=node_id, node_name=node_name, + type='trigger_function', nspname=nspname) + status, res = conn.execute_dict(SQL) + + if not status: + return internal_server_error(errormsg=res) + + res_data.extend(res['rows']) + + # Fetch Sequences against schema + if node_type in ['schema', 'sequence']: + SQL = render_template("/".join( + [template_path, 'properties.sql']), + node_id=node_id, node_name=node_name, + type='sequence', nspname=nspname) + + status, res = conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + res_data.extend(res['rows']) + + # Fetch Tables against schema + if node_type in ['schema', 'table']: + SQL = render_template("/".join( + [template_path, 'properties.sql']), + node_id=node_id, node_name=node_name, + type='table', nspname=nspname) + + status, res = conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + res_data.extend(res['rows']) + + # Fetch Views against schema + if node_type in ['schema', 'coll-view']: + SQL = render_template("/".join( + [template_path, 'properties.sql']), + node_id=node_id, node_name=node_name, + type='coll-view', nspname=nspname) + + status, res = conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + res_data.extend(res['rows']) + + return ajax_response( + response=res_data, + status=200 + ) + + +@blueprint.route( + '/grantwizard/msql//////', + methods=('GET', 'POST')) +def msql(gid, sid, did, node_type, nspname): + """ + This function will return modified SQL + """ + data = {} + for k, v in request.args.items(): + try: + data[k] = json.loads(v) + except ValueError: + data[k] = v + + # Form db connection + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection(did=did) + + # we will set template path for sql scripts + template_path = 'grantwizard/sql' + + acls = [] + try: + acls = render_template( + "/".join([template_path, 'allowed_acl.json']) + ) + acls = json.loads(acls) + except Exception as e: + current_app.logger.exception(e) + + try: + # Parse Privileges + if 'acl' in data: + data['acl'] = parse_priv_to_db(data['acl'], acls[node_type]['acl']) + + # Pass Database Objects and get SQL for privileges + SQL = render_template("/".join( + [template_path, 'grant.sql']), + data=data, nspname=nspname, conn=conn) + + res = {'data': SQL} + + return ajax_response( + response=res, + status=200 + ) + + except Exception as e: + return make_json_response( + status=410, + success=0, + errormsg=e.message + ) + + +@blueprint.route( + '/grantwizard/save////' + '//', + methods=('GET', 'POST')) +def save(gid, sid, did, node_type, nspname): + """ + This function will apply the privileges to the selected + Database Objects + """ + data = request.form if request.form else json.loads(request.data.decode()) + + # Form db connection and we use conn to execute sql + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection(did=did) + + # we will set template path for sql scripts + template_path = 'grantwizard/sql' + + acls = [] + try: + acls = render_template( + "/".join([template_path, 'allowed_acl.json']) + ) + acls = json.loads(acls) + except Exception as e: + current_app.logger.exception(e) + + try: + + # Parse privileges + if 'acl' in data: + data['acl'] = parse_priv_to_db( + data['acl'], + acls[node_type]['acl'] + ) + + # Pass Database Objects and get SQL for privileges + SQL = render_template("/".join( + [template_path, 'grant.sql']), + data=data, nspname=nspname, conn=conn) + + if SQL and isinstance(SQL, basestring) and \ + SQL.strip('\n') and SQL.strip(' '): + status, res = conn.execute_dict(SQL) + if not status: + return internal_server_error(errormsg=res) + + return make_json_response( + success=1, + info="Privileges Applied" + ) + + except Exception as e: + return make_json_response( + status=410, + success=0, + errormsg=e.message + ) diff --git a/web/pgadmin/tools/grantwizard/static/css/grantwizard.css b/web/pgadmin/tools/grantwizard/static/css/grantwizard.css new file mode 100644 index 0000000..953e788 --- /dev/null +++ b/web/pgadmin/tools/grantwizard/static/css/grantwizard.css @@ -0,0 +1,48 @@ +/* Grant Wizard CSS */ + +/* CSS to make Db object type table + * fixed so that tbody content may + * scroll + */ +.object_type_table thead tr { + position: relative; + display: block; +} + +.object_type_table tbody { + display: block; + overflow-y: scroll; + height: 160px; +} + +.object_type_table tbody tr td:nth-child(1), +.object_type_table thead tr th:nth-child(1) { + width: 28px; + min-width: 28px; +} + +.object_type_table tbody tr td:nth-child(2), +.object_type_table thead tr th:nth-child(2) { + width: 185px; + min-width: 185px; +} + +.object_type_table tbody tr td:nth-child(3), +.object_type_table thead tr th:nth-child(3) { + width: 110px; + min-width: 110px; +} + +.object_type_table tbody tr td:nth-child(4), +.object_type_table thead tr th:nth-child(4) { + width: 244px; +} + +.error_msg_div { + display:block; +} + +/** Override Backgrid filter CSS **/ +.backgrid-filter.form-search { + margin: 0px 5px 5px 0; +} diff --git a/web/pgadmin/tools/grantwizard/static/css/wizard.css b/web/pgadmin/tools/grantwizard/static/css/wizard.css new file mode 100644 index 0000000..058afa7 --- /dev/null +++ b/web/pgadmin/tools/grantwizard/static/css/wizard.css @@ -0,0 +1,86 @@ +/** CSS for Wizard **/ +.wizard_dlg { + height: 300px; + padding: 0 10px; +} + +.ajs-content { + padding-top: 0px !important; +} + +/* Wizard Header CSS */ +.wizard-header { + background: #428bca; + padding-left: 15px; + padding-bottom: 7px; + color: #fff; + font-size: 18px; + margin-bottom: 15px; + height: 76px; + line-height: 76px; +} + +.wizard-header h3 { + font-size: 18px; + display: inline-block; +} + +.wizard-content { + overflow-y: auto; + float: left; + padding: 0; + min-height: 280px; + max-height: 280px; +} + +/* Wizard Footer CSS */ +.footer { + position: absolute; + border-top: 1px solid #ccc; + bottom: 0px; + height: 71px; + right: 0px; + padding-top: 22px; +} + +/* Wizard Button CSS */ +.wizard-buttons { + float: right; +} + +.wizard-buttons button { + float: left; + padding: 7px 15.2px; + font-size: 14px; + margin-right: 5px; +} + +.wizard-finish { + margin-right: 0; +} + +/* Wizard Status bar CSS */ +.wizard-status-bar { + height: 50px; + padding-bottom: 10px; + margin-bottom: 10px; + padding-left: 15px; +} + +/* Error message css */ +.error_msg_div, +.error_msg_div .pg-prop-status-bar { + background: #fff; + display: none; +} + +/* In wizard select2 dropdown doesn't + * popup because z-index of alertify + * wizard is greater than the z-index + * of select2 dropdown. To make select2 + * visible, set z-index of select2 + * higher value than wizard's + */ +.select2-container--open { + z-index: 10000; +} diff --git a/web/pgadmin/tools/grantwizard/static/img/coll-extension.png b/web/pgadmin/tools/grantwizard/static/img/coll-extension.png new file mode 100644 index 0000000000000000000000000000000000000000..eed7ca97a33ef595f448b8621168d531f86d51d5 GIT binary patch literal 1017 zcmVPx%_fSk!MS^xZ(#^N3p?R&Od+_Yu$ibj%T^6RorB#bmn!1_5x}2JiUH9|m=i$p{ zRt>7esZx(rkHMB&m|kp}YkF-i+S9qHoNTYCfx^|oK7c>H>Bx7neMEjjfO9vZnQgzg znaszo-`vc-ww7K_0rBeAwyS=gm1jPCJeJI$Gj=n&vyq*XV~~VSeQ+?GkzLu)v+(KC z?&Zpui%@D?5_o7K5L*te*2!a-MZ%4vg6gM;L)L= zk53w479M3BDQ+lfsBo&|xoE3!8)F$jd_LXGn5dUmHEJcE$)m~b(xK3*v*EqB=fkk& zzN6u@o!_pI*P(X7g>k=tf76xP$dIj?SVe9ugSLpn>&}+lsF2#DiP)Wj)|h*Toyk;|+MLMZbK)_+XojHAH5m&nL&4HC>d%SP=`*4d_$3X zCoGaZfT(|KS__tZB7UZQKD%8OXc&KL7J+LO8+Rm0f=C!{9T#mJ7UDFE00001bW%=J z06^y0W&i*H0b)x>L;#2d9Y_EG010qNS#tmY3ljhU3ljkVnw%H_000McNliru+XEXB z6Cp(Rbcp}}0B%V{K~xyiU68R2z#t3+nS(tt!3~hY^vGg42FL=`EENNm0{M!6-y1;) zQxFazvL_cK*b<_okFg0d8Hc#V=Eh`)h+yZmplehnlNZEdtgQ@hX0CNeIWEpxw!oyN ndc~tsp9TlidOzOC&-)*|KH)3pD2ngr00000NkvXXu0mjfPx%`cO<%MQ2zLf_6I6&9|zdd99>-@a*2m!Junh7N)|bRf|-bx|zVboSKha_w(lG z;mc-L4XVVcQjb)R!IoN>UTm6cdTlP+)48aeY_F(+!qvh)fIq$I$ak@QM1Demb2p-y zZNIpg%*U?Z+|0eUmR?Q)@#@yLtA3u9XFhv8mdv0tb~C!Ok)4xckc3Wsa4?*aUD?pH z@afX-<;s|gP-A^X%E{;lk+My5`xk#6?p?1NAale3n)0NuDkgb|nMQ$vEwur;)&X(P%klLe(*qwsbn0(Ze z&%lqBg+*3S1ZY?dnutTImr{GNfpyZ2etkM9XCNqTCtr9wihVOAV;iN_u6D(TK#xcf zTn`v!7(bOtw4`b=XCQ>Lht=`it<|(#mtSF?Wm1BZ;G0*SUx8%WGt7&4B zV3D+tb8RIwgF3Luu#bN^7G4v==*yVRp-_@kjmeoSdoaG!zSzWv!K`UHUKC!MVuZSj zaIAJVdpAyF9vxyClh&g+jY8GGe$u;klYuouT@*iVD?w)>Y;7EsekVC?DRR1gZ_tQZ zyKv99ZOyY~z@}84hBK{_JHn(!l6fLVbTnhFZfMSfV$6Lul0qhGC2*Z?NP$a6}(W;2>aI;l}Wz+t|fIeljlScFSfreaXWZ%f2# zMZ#t~zg?k^OOAUphI1*4cr3V}Qns8&igY1@Y#D@Y7H?b$T8UCtsbn_2S%i2!rk!)h z!>EaMCcvamyR?(W!KK8$qF<3+9BCaAUJ)8<9X6LmEss1DV->ZTL3e2=8EP9)hfaum zLy>wXERsEdsDEo(3zmE$ex`jsyImG&7=LOOfol~TcO*%INEmJ%7i}B|YRn1%0004W zQchC0W=QhVA)yH_FkL{}1;huk zpaKCQKz@=7s`{i97i0@vl2TS8w1C7?R&G7y;)1Nm< 1){ + $('.ajs-content')[0].remove(); + } + + // Generate wizard main container + var wizard_html = '
', + wizard_parent = $('.ajs-content').empty().append(wizard_html), + el = $(".ajs-content"); + + // Extract the data from the selected tree node + var t = pgBrowser.tree, + i = t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined, + info = pgBrowser.Node.getTreeNodeHierarchy(i), + icon = d.icon; + + /** + Generate a URL using: + gid, did, sid(server id), node_id(node id), + node_(node name), node_type(node type) + and pass it to collection which will fetch Object Type properties. + */ + var gid = info['server-group']._id, + sid = info.server._id, + did = info.database._id, + node_id = d._id, + nspname = info.schema.label; + node_type = d._type, + node_label = d.label, + + // Collection url to fetch Database object types for objects field + baseUrl = "{{ url_for('grantwizard.index') }}" + "grantwizard/properties/" + + S('%s/%s/%s/%s/%s/%s/%s/').sprintf( + encodeURI(gid), encodeURI(sid), encodeURI(did), + encodeURI(node_id), encodeURI(node_label), + encodeURI(node_type), encodeURI(nspname)).value(); + + // Model's save url + saveUrl = "{{ url_for('grantwizard.index') }}" + "grantwizard/save/" + + S('%s/%s/%s/%s/%s/').sprintf( + encodeURI(gid), encodeURI(sid), encodeURI(did), + encodeURI(node_type), encodeURI(nspname)).value(), + + // Create instances of collection, pagination and filter + coll = this.coll = this.getPageableCollection(baseUrl), + paginator = this.paginator = this.DbPaginator(coll), + dbObjectFilter = this.dbObjectFilter = this.DbObjectFilter(coll); + + /** + It is the main model with schema defined + Every time a new wizard is opened, + a new model should create. + */ + var GrantWizardModel = pgNode.Model.extend({ + defaults: { + objects: undefined, + acl: undefined + }, + schema: [ + { + id: 'objects', label: '{{ _("Objects") }}', model: DatabaseObjectModel, + type: 'collection', group: 'Objects' + }, + { + id: 'acl', label: '{{ _("Privileges") }}', node: 'role', + model: pgAdmin.Browser.Node.PrivilegeRoleModel.extend({ + privileges: privDict[node_type] + }), uniqueCol : ['grantee', 'grantor'], editable: true, + type: 'collection', group: 'Privileges', canAdd: true, + canDelete: true, control: 'unique-col-collection' + } + ], + urlRoot: saveUrl + }); + + /** + Create instance of GrantWizard Model, provide urlRoot + node_info object, Generate fields objects + */ + var self = this, + newModel = new GrantWizardModel({}, { node_info: info }), + fields = Backform.generateViewSchema( + info, newModel, 'create', d._type, d + ); + + /** + Fetch data from server and set into grid + and show/hide progress bar + */ + $('.wizard-progress-bar p').show(); + + coll.fetch({ + success: function(collection, data) { + $('.wizard-progress-bar p').html(''); + }, + reset: true + }, this); + + /** + Override backgrid listener "backgrid:selected" to + Add/Remove model to/from objects collection + */ + coll.on('backgrid:selected', function(model, selected) { + model.set('selected', selected); + + /** + if a row (checkbox) is checked, add that model + into collection, when unchecked remove it from + model. + */ + if(selected) + newModel.get('objects').add(model, { silent: true }); + else + newModel.get('objects').remove(model); + + // validate model on checkbox check/uncheck + var msg = model.validate.call(newModel); + + /** + If no object type is selected, set error msg + and disable next button, else enable next button + */ + if(msg) + self.onSessionInvalid.call(self, msg); + else + self.onSessionValidated.call(self, true); + }); + + // To track changes into model, start new session + newModel.startNewSession(); + + // Add event listener for privileges control + newModel.on('pgadmin-session:valid', self.onSessionValidated.bind(this)); + newModel.on('pgadmin-session:invalid', self.onSessionInvalid.bind(this)); + + ////////////////////////////////////////////////////////////////////// + // // + // Wizard Page for Db Object Type // + // // + ////////////////////////////////////////////////////////////////////// + + /** + Create wizard page. It renders a grid of + Database Object Types such as + Schemas, Views and Sequences etc. + Set default values + */ + var dbObjectTypePage = self.dbObjectTypePage = new pgBrowser.WizardPage({ + id: 1, + page_title: 'Object Selection (step 1 of 3)', + disable_prev: true, + disable_next: true, + show_status_bar: _('Please select objects from the below list.'), + show_progress_bar: _('Please wait while fetching records...'), + model: newModel, + view: new (function() { + + // Set page Instance + var pageView = this; + + _.extend(pageView, { + + // Remove grid if it is before render + cleanup: function() { + if (this.grid) { + this.grid.remove(); + delete this.grid; + this.grid = null; + } + + // Remove grid element if exists + if (this.el) { + $(this.el).remove(); + delete this.el; + } + }, + + // Delete grid before render + grid: null, + + // Render the Page1 + render: function() { + + // Create a grid container + var gridBody = + $('
'); + + // Remove grid if exits before render + if (this.grid) { + this.cleanup(); + } + + // Initialize a new Grid instance + this.grid = new Backgrid.Grid({ + columns: _.clone(columns), + collection: coll, + className: "backgrid table-bordered object_type_table" + }); + + // Render selection Type grid and paginator + gridBody.append( + this.grid.render().$el + ).append(self.paginator.render().el); + + // Render Search Filter + gridBody.prepend( + self.clientSideFilter.render().el); + + // Assign gridBody content to page element + this.el = gridBody; + + /** + Fetch selected models from collection and + make rows checked in grid + */ + newModel.get('objects').each(function(m) { + var model = coll.get(m.get('object_id')); + if (model) { + coll.trigger('backgrid:selected', model, true); + } + }); + + // Refresh grid to re render rows. + coll.trigger('backgrid:refresh'); + + return this; + } + }); + }), + + beforeNext: function(obj){ + var self = this, + selectedRows = self.view.grid.getSelectedModels(); + obj.options.disable_next = true; + + /** + Enable/Disable next button of page 2 if objects + are present in model + */ + if(newModel.get('acl').length > 0) + obj.collection.at(1).set('disable_next', false); + + // Clean the view + if (self.view) { + self.view.cleanup(); + delete self.view; + self.view = null; + } + return true; + }, + + }); + + ////////////////////////////////////////////////////////////////////// + // // + // Wizard Page for Privilege Control // + // // + ////////////////////////////////////////////////////////////////////// + + /** + Create Field Object which has properties like + node_data, node_info which is required for rendering + Privilege control + */ + var privilegesField = new Backform.Field(fields[1].fields[0]); + + // Wizard for Privelege control + var privilegePage = self.privilegePage = new pgBrowser.WizardPage({ + id: 2, + page_title: _('Privileges Selection (step 2 of 3)'), + show_status_bar: _('Please select privileges for the selected objects.'), + disable_next: true, + model: newModel, + + // Create a view function object + view: new (function() { + var pageView = this; + _.extend(pageView, { + + // Render Privelege control to generate its html markup + render: function() { + this.privControl = new (privilegesField.get('control')) ({ + field: privilegesField, + model: newModel + }); + return {el: this.privControl.render().$el}; + }, + + // Remove the privilege control + cleanup: function() { + if (this.privControl) { + this.privControl.remove(); + delete this.privControl; + this.privControl = null; + } + } + }); + }), + + beforePrev: function(wizardObj) { + + // Remove the privilege control + if (this.view) { + this.view.cleanup(); + delete this.view; + this.view = null; + } + + /** + Enable/Disable next button of page 1 if objects + are present in model + */ + var objectsModel = newModel.get('objects'); + + if(!_.isUndefined(objectsModel) && !_.isEmpty(objectsModel) && + objectsModel.length > 0) { + wizardObj.collection.at(0).set('disable_next', false); + + // Don't show progress bar + wizardObj.collection.at(0).set('show_progress_bar', ''); + } + + /** + We're re-rendering the controls as they are deleted + before heading to next page + Refresh Backgrid to re-render the elements selected + re-render paginator + re-render Filter + */ + newModel.trigger("backgrid:refresh", newModel, false); + self.paginator.render(); + self.clientSideFilter.render(); + return true; + }, + + beforeNext: function() { return true; }, + + onNext: function(obj){ + + // Remove the privilege control + if (this.view) { + this.view.cleanup(); + delete this.view; + this.view = null; + } + + // Enable finish button on Wizard Page 3 + self.wizard.options.disable_finish = false; + + // Triggers to get SQL queries data to render into the + // Wiard SQL Tab Control + this.model.trigger('pgadmin-wizard:nextpage:sql', + {'wizard_type': 'grantwizard', 'node_type': node_type }); + } + }); + + + ////////////////////////////////////////////////////////////////////// + // // + // Wizard Page for SQL Tab // + // // + ////////////////////////////////////////////////////////////////////// + + //Create SqlField Object + var sqlField = new Backform.Field( + { + id: 'sqltab', + label: 'Sql Tab', + field: fields, + control: Backform.SqlTabControl + }), + + /** + Create sqlField view instance + to render it into wizard page + */ + sqlControl = new (sqlField.get('control'))({ + field: sqlField, + model: newModel + }); + + // Wizard for SQL tab control + var reviewSQLPage = self.reviewSQLPage = new pgBrowser.WizardPage({ + id: 3, + page_title: _('Final (Review Selection) (step 3 of 3)'), + show_status_bar: _('Following query will be executed on the database' + +' server for the selected objects, and privileges.' + +' Please click on Finish to complete the process.'), + + view: new(function() { + + // Render SqlTab control to generate its html markup + var sqlTabHtml = sqlControl.render().$el; + this.render = function() { + return {el: sqlTabHtml}; + }; + }), + + beforePrev: function(wizardObj) { + + /** + Enable next button on page 2 + if privilege model is not empty + else disable next button + */ + var aclModel = newModel.get('acl'); + + if(!_.isUndefined(aclModel) && !_.isEmpty(aclModel) && + aclModel.length > 0) + wizardObj.collection.at(1).set('disable_next', false); + else + wizardObj.collection.at(1).set('disable_next', true); + + return true; + }, + }); + + + // Create Wizard Collection of Wizard Pages + var WizardCollection = Backbone.Collection.extend({ + model: pgBrowser.WizardPage + }); + + // It holds all the wizard pages to be rendered + this.wizardCollection = new WizardCollection( + [dbObjectTypePage, privilegePage, reviewSQLPage] + ); + + /** + Create wizard which has following operations: + - renders wizard pages + - defines the first page to render in wizard + - Save the model on finishbutton + - Remove wizard on cancel button + */ + var wizard = self.wizard = new (pgBrowser.Wizard.extend({ + options: { + title: 'Grant Wizard', /* Main Wizard Title */ + width: '', + height: '', + curr_page: 0, + show_left_panel: false, + disable_finish: true + }, + onFinish: function() { + var m = newModel, + d = m.toJSON('GET'); + + if (d && !_.isEmpty(d)) { + m.save({}, { + attrs: d, + validate: false, + cache: false, + success: function(res) { + + // Release wizard objects + self.close(); + }, + error: function(m, jqxhr) { + alertify.pgNotifier( + "error", jqxhr, + S( + "{{ _('Error during saving properties - %%s!') }}" + ).sprintf(jqxhr.statusText).value() + ); + } + }); + } + }, + onCancel: function() { + + // Release wizard objects + self.releaseObjects(); + self.close(); + } + })) ({ + collection: this.wizardCollection, + el: el, + model: newModel + }); + + // Render wizard + wizard.render(); + } + }; + }); + } + + // Call Grant Wizard Dialog + alertify.wizardDialog(''); + } + }; + + return pgBrowser.GrantWizard; +}); diff --git a/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/allowed_acl.json b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/allowed_acl.json new file mode 100644 index 0000000..cb73d2c --- /dev/null +++ b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/allowed_acl.json @@ -0,0 +1,30 @@ +{# List of allowed privileges of Schema, table sequence, function and view #} +{# + Format for allowed privileges are: + "node_name": { + "type": "name", + "acl": [...] + } +#} +{ + "schema": { + "type": "SCHEMA", + "acl": ["a", "r", "w", "d", "D", "x", "t", "U", "X"] + }, + "coll-table": { + "type": "TABLE", + "acl": ["a", "w", "U", "d", "D", "x", "t"] + }, + "coll-sequence": { + "type": "SEQUENCE", + "acl": ["a", "w", "u"] + }, + "coll-function": { + "type": "FUNCTION", + "acl": ["X"] + }, + "coll-view": { + "type": "VIEW", + "acl": ["a", "r", "w", "d", "D", "x", "t"] + } +} diff --git a/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/grant.sql b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/grant.sql new file mode 100644 index 0000000..02ac1a7 --- /dev/null +++ b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/grant.sql @@ -0,0 +1,19 @@ +{#=====Grant Permissions on Database Objects Selected====#} +{% import 'macros/schemas/privilege.macros' as PRIVILEGE %} +{% import 'macros/functions/privilege.macros' as PRIVILEGE_FUNCTION %} +{% if data.acl %} +{% for obj in data.objects %} +{% for priv in data.acl %} +{% set obj_type = obj.object_type %} +{% if obj_type.upper() != 'FUNCTION' and obj_type.upper() != 'TRIGGER FUNCTION' %} +{% if obj_type.upper() == 'VIEW' %} {###### Views are also kind of table ######} +{% set obj_type = 'TABLE' %} +{% endif %} +{{ PRIVILEGE.SET(conn, obj_type.upper(), priv.grantee, obj.name, priv.without_grant, priv.with_grant, nspname ) }} +{% else %} +{###### if object_type is Function then apply function marcros ######} +{{ PRIVILEGE_FUNCTION.SET(conn, 'FUNCTION', priv.grantee, obj.name, priv.without_grant, priv.with_grant, nspname, obj.proargs)}} +{% endif %} +{% endfor %} +{% endfor %} +{% endif %} diff --git a/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/properties.sql b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/properties.sql new file mode 100644 index 0000000..7a33c1f --- /dev/null +++ b/web/pgadmin/tools/grantwizard/templates/grantwizard/sql/properties.sql @@ -0,0 +1,65 @@ +{#===================Fetch list of Database object types(Functions, Sequence, Tables or View)===================#} +{% if type == 'schema' or type == 'trigger_function' and node_id and node_name %} +{% set func_type = 'Trigger Function' if type == 'trigger_function' else 'Function' %} +SELECT + pr.proname AS name, + pg_get_function_identity_arguments(pr.oid) AS proargs, + '{{ func_type }}' AS object_type, + '{{ nspname }}' AS nspname, + 'icon-function' AS icon +FROM pg_proc pr + JOIN pg_type typ ON typ.oid=prorettype + JOIN pg_namespace typns ON typns.oid=typ.typnamespace + JOIN pg_language lng ON lng.oid=prolang + LEFT OUTER JOIN pg_description des ON (des.objoid=pr.oid AND des.classoid='pg_proc'::regclass) +WHERE proisagg = FALSE AND pronamespace = {{ node_id }}::oid + AND typname {{ 'NOT' if type != 'trigger_function' else '' }} IN ('trigger', 'event_trigger') +ORDER BY proname +{% endif %} + +{% if type == 'sequence' and node_id and node_name %} +SELECT + cl.relname AS name, + 'Sequence' AS object_type, + '{{ nspname }}' AS nspname, + 'icon-sequence' AS icon +FROM pg_class cl + LEFT OUTER JOIN pg_description des ON (des.objoid=cl.oid AND des.classoid='pg_class'::regclass) +WHERE relkind = 'S' AND relnamespace = {{ node_id }}::oid +ORDER BY cl.relname +{% endif %} + +{% if type == 'table' and node_id and node_name %} +SELECT rel.relname AS name, + 'Table' AS object_type, + '{{ nspname }}' AS nspname, + 'icon-table' AS icon +FROM pg_class rel + LEFT OUTER JOIN pg_tablespace spc ON spc.oid=rel.reltablespace + LEFT OUTER JOIN pg_description des ON (des.objoid=rel.oid AND des.objsubid=0 AND des.classoid='pg_class'::regclass) + LEFT OUTER JOIN pg_constraint con ON con.conrelid=rel.oid AND con.contype='p' + LEFT OUTER JOIN pg_class tst ON tst.oid = rel.reltoastrelid + LEFT JOIN pg_type typ ON rel.reloftype=typ.oid +WHERE rel.relkind IN ('r','s','t') AND rel.relnamespace = {{ node_id }}::oid +ORDER BY rel.relname +{% endif %} + +{% if type == 'coll-view' and node_id and node_name %} +SELECT + c.relname AS name, + 'View' AS object_type, + '{{ nspname }}' AS nspname, + 'icon-coll-view' AS icon +FROM pg_class c + LEFT OUTER JOIN pg_tablespace spc ON spc.oid=c.reltablespace + LEFT OUTER JOIN pg_description des ON (des.objoid=c.oid and des.objsubid=0 AND des.classoid='pg_class'::regclass) + LEFT OUTER JOIN pg_class tst ON tst.oid = c.reltoastrelid +WHERE ((c.relhasrules AND (EXISTS ( + SELECT r.rulename + FROM pg_rewrite r + WHERE ((r.ev_class = c.oid) + AND (bpchar(r.ev_type) = '1'::bpchar)) )) + ) OR (c.relkind = 'v'::char)) +AND c.relnamespace = {{ node_id }}::oid +ORDER BY c.relname +{% endif %}