diff --git a/web/pgadmin/browser/static/css/wizard.css b/web/pgadmin/browser/static/css/wizard.css
new file mode 100644
index 0000000..ca617b6
--- /dev/null
+++ b/web/pgadmin/browser/static/css/wizard.css
@@ -0,0 +1,148 @@
+
+/** CSS for Wizard **/
+.ajs-content {
+ padding-top: 0px !important;
+}
+
+/* Wizard Header CSS */
+.alertify .ajs-dialog .ajs-close {
+ margin-top: 15px;
+}
+
+.wizard-header {
+ background: #428bca;
+ padding-left: 15px;
+ padding-bottom: 7px;
+ color: #fff;
+ font-size: 18px;
+ height: 76px;
+ line-height: 76px;
+ border-radius: 6px 6px 0 0;
+}
+
+.wizard-header h3 {
+ font-size: 18px;
+ display: inline-block;
+}
+
+.wizard_dlg {
+ float: left;
+ height: 100%;
+ width: 100%;
+}
+
+.grant_wizard_container {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+.pgadmin-wizard {
+ width: 100%;
+ height: 100%;
+}
+
+.wizard-content {
+ position: relative;
+ padding: 0;
+ height: 64%;
+}
+
+.wizard-right-panel {
+ overflow-y: auto;
+ height: 100%;
+ top: 0;
+ right: 0;
+ position: absolute;
+ bottom: 0;
+}
+
+.wizard-left-panel {
+ position: absolute;
+ top: 0;
+ display: flex;
+ bottom: 0;
+ left: 0;
+ align-items: center;
+ justify-content: center;
+ right: 0;
+}
+
+.wizard-left-panel img {
+ width: 140px;
+}
+
+.wizard-right-panel_content {
+ height: 60%;
+}
+
+.select_db_objects_container {
+ height: 100%;
+}
+
+/* Wizard Footer CSS */
+.footer {
+ position: absolute;
+ background: #fff;
+ border-top: 1px solid #ccc;
+ bottom: 0px;
+ height: 62px;
+ right: 0px;
+ padding-top: 22px;
+ border-radius: 0 0 6px 6px;
+ z-index: 10;
+}
+
+/* Wizard Button CSS */
+.wizard-buttons {
+ float: right;
+}
+
+.wizard-buttons button {
+ float: left;
+ padding: 7px 15.2px;
+ font-size: 14px;
+ margin: 0px 5px 0 0 !important;
+}
+
+.wizard-buttons button.wizard-next i.fa {
+ padding-left: 5px;
+}
+
+.wizard-buttons button.wizard-back i.fa,
+.wizard-buttons button.wizard-cancel i.fa {
+ padding-right: 5px;
+}
+
+.wizard-buttons .wizard-finish {
+ margin-right: 0 !important;
+}
+
+/* Wizard Status bar CSS */
+.wizard-description {
+ padding: 1.7em 0.1em;
+}
+
+/* Error message css */
+.error_msg_div {
+ display: block;
+ position: absolute;
+ bottom: 55px;
+ background: #fff;
+}
+
+.error_msg_div p {
+ background: #fff;
+ color: #b92c28;
+}
+
+/* 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/browser/static/js/node.ui.js b/web/pgadmin/browser/static/js/node.ui.js
index f127d52..a800593 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/browser/static/js/wizard.js b/web/pgadmin/browser/static/js/wizard.js
new file mode 100644
index 0000000..f42eab7
--- /dev/null
+++ b/web/pgadmin/browser/static/js/wizard.js
@@ -0,0 +1,226 @@
+define(
+ ['underscore', 'backbone', 'pgadmin', 'pgadmin.browser'],
+function(_, Backbone, pgAdmin, pgBrowser) {
+
+ pgBrowser = pgBrowser || pgAdmin.Browser || {};
+
+ /* Wizard individual Page Model */
+ var WizardPage = pgBrowser.WizardPage = Backbone.Model.extend({
+ defaults: {
+ id: undefined, /* Id */
+ page_title: undefined, /* Page Title */
+ view: undefined, /* A Backbone View */
+ html: undefined, /* HTML tags to be rendered */
+ image: undefined, /* Left hand side image */
+ disable_prev: false, /* Previous Button Flag */
+ disable_next: false, /* Next Button Flag */
+ disable_cancel: false, /* Cancel Button Flag */
+ show_progress_bar: '',
+ /* Callback for OnLoad */
+ onLoad: function() {
+ return true;
+ },
+ /* Callback for before Next */
+ beforeNext: function() {
+ return true;
+ },
+ onNext: function(){},
+ onBefore: function() {},
+ /* Callback for before Previous */
+ beforePrev: function() {
+ return true;
+ }
+ }
+ });
+
+ var Wizard = pgBrowser.Wizard = Backbone.View.extend({
+ options: {
+ title: 'Wizard', /* Main Wizard Title */
+ image: 'left_panel.png', /* TODO:: We can use default image here */
+ curr_page: 0, /* Current Page to Load */
+ disable_next: false,
+ disable_prev: false,
+ disable_finish: false,
+ disable_cancel: false,
+ height: 400,
+ width: 650,
+ show_left_panel: true
+ },
+ tmpl: _.template(
+ "
"
+ + " "
+ + "
"
+ + " <% if(this.options.show_left_panel) { %>"
+ + "
"
+ + "

"
+ + " <% } %>"
+ + "
"
+ + " <% if( typeof show_description != 'undefined'){ %>"
+ + "
"
+ + " <%= show_description %>"
+ + "
"
+ + " <% } %>"
+ + "
"
+ + "
"
+ + "
"
+ + "
"
+ + "
"
+ + " "
+ + "
"),
+ events: {
+ "click button.wizard-next" : "nextPage",
+ "click button.wizard-back" : "prevPage",
+ "click button.wizard-cancel" : "onCancel",
+ "click button.wizard-finish" : "finishWizard",
+ },
+ initialize: function(options) {
+ this.options = _.extend({}, this.options, options.options);
+ this.currPage = this.collection.at(this.options.curr_page).toJSON();
+ },
+ render: function() {
+ var data = this.currPage;
+
+ /* Check Status of the buttons */
+ this.options.disable_next = (this.options.disable_next ? true : this.evalASFunc(this.currPage.disable_next));
+ this.options.disable_prev = (this.options.disable_prev ? true : this.evalASFunc(this.currPage.disable_prev));
+ this.options.disable_cancel = (this.currPage.canCancel ? true : this.evalASFunc(this.currPage.disable_cancel));
+
+ that = this;
+
+ /* HTML Content */
+ if (data.html) { data.content = data.html; }
+ /* Backbone View */
+ else if (data.view) { data.content = data.view.render().el;}
+
+ $(this.el).html(this.tmpl(data));
+ $(this.el).find(".wizard-right-panel_content").html(data.content);
+
+ /* OnLoad Callback */
+ this.onLoad();
+
+ return this;
+ },
+ nextPage: function() {
+ this.options.curr_page.el = this.$el;
+ if (!this.beforeNext()) { return false; }
+
+ page_id = this.onNext();
+
+ if (page_id ) {
+ this.currPage = this.collection.get(page_id).toJSON();
+ this.options.curr_page = this.collection.indexOf(this.collection.get(page_id));
+ }
+ else if (this.options.curr_page < (this.collection.length-1)) {
+ this.options.curr_page = this.options.curr_page + 1;
+ this.currPage = this.collection.at(this.options.curr_page).toJSON();
+ }
+
+ this.enableDisableNext();
+ this.enableDisablePrev();
+
+ return this.render();
+ },
+ prevPage: function() {
+ if (!this.beforePrev()) { return false; }
+
+ page_id = this.onPrev();
+
+ if (page_id){
+ this.currPage = this.collection.get(page_id).toJSON();
+ this.options.curr_page = this.collection.indexOf(this.collection.get(page_id));
+ }
+ else if (this.options.curr_page > 0) {
+ this.options.curr_page = this.options.curr_page - 1;
+ this.currPage = this.collection.at(this.options.curr_page).toJSON();
+ }
+
+ this.enableDisableNext();
+ this.enableDisablePrev();
+
+ return this.render();
+ },
+ finishWizard: function() {
+ this.onFinish();
+ this.remove(); // Remove view from DOM
+ this.unbind(); // Unbind all local event bindings
+ delete this.$el; // Delete the jQuery wrapped object variable
+ delete this.el; // Delete the variable reference to this node
+ return true;
+ },
+ enableDisableNext: function(disable) {
+ if (typeof(disable) != 'undefined') {
+ this.options.disable_next = disable;
+ }
+ else if (this.options.curr_page >= (this.collection.length-1)) {
+ this.options.disable_next = true;
+ }
+ else {
+ this.options.disable_next = false;
+ }
+ },
+ enableDisablePrev: function(disable) {
+ if (typeof(disable) != 'undefined') {
+ this.options.disable_prev = disable;
+ }
+ else if (this.options.curr_page <= 0) {
+ this.options.disable_prev = true;
+ }
+ else {
+ this.options.disable_prev = false;
+ }
+ },
+ beforeNext: function(){
+ return this.evalASFunc(this.currPage.beforeNext);
+ },
+ beforePrev: function(){
+ return this.evalASFunc(this.currPage.beforePrev);
+ },
+ onPrev: function(){
+ return this.evalASFunc(this.currPage.onPrev);
+ },
+ onNext: function(){
+ return this.evalASFunc(this.currPage.onNext);
+ },
+ onLoad: function() {
+ return this.evalASFunc(this.currPage.onLoad);
+ },
+ onFinish: function() {
+ return true;
+ },
+ onCancel: function() {
+ this.$el.remove();
+ return true;
+ },
+ evalASFunc: function(func, ctx) {
+ var self = this;
+ ctx = ctx || self.currPage;
+
+ return (_.isFunction(func) ? func.apply(ctx, [self]) : func);
+ }
+ });
+
+ return pgBrowser;
+
+});
diff --git a/web/pgadmin/tools/grant_wizard/__init__.py b/web/pgadmin/tools/grant_wizard/__init__.py
new file mode 100644
index 0000000..c71818e
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/__init__.py
@@ -0,0 +1,386 @@
+##########################################################################
+#
+# 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
+from urllib import unquote
+
+# set template path for sql scripts
+template_path = 'grant_wizard/sql'
+MODULE_NAME = 'grant_wizard'
+
+
+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_stylesheets(self):
+ """
+ Returns:
+ list: the stylesheets used by this module.
+ """
+ stylesheets = [
+ url_for('browser.static', filename='css/wizard.css'),
+ url_for('grant_wizard.static', filename='css/grant_wizard.css')
+ ]
+ return stylesheets
+
+ def get_own_javascripts(self):
+ """"
+ Returns:
+ list: js files used by this module
+ """
+ scripts = []
+ scripts.append({
+ 'name': 'pgadmin.tools.grant_wizard',
+ 'path': url_for('grant_wizard.index') + 'grant_wizard',
+ 'when': None
+ })
+ scripts.append({
+ 'name': 'pgadmin.browser.wizard',
+ 'path': url_for('browser.static', filename='js/wizard'),
+ 'when': None
+ })
+ return scripts
+
+# Create blueprint for GrantWizardModule class
+blueprint = GrantWizardModule(
+ MODULE_NAME, __name__, static_url_path='')
+
+
+@blueprint.route("/")
+@login_required
+def index():
+ pass
+
+
+@blueprint.route("/grant_wizard.js")
+@login_required
+def script():
+ """render own javascript"""
+ return Response(response=render_template(
+ "grant_wizard/js/grant_wizard.js", _=gettext),
+ status=200,
+ mimetype="application/javascript")
+
+
+@blueprint.route("/allowed_acl.json")
+@login_required
+def acl_list():
+ """render list of acls"""
+ return Response(response=render_template(
+ "grant_wizard/sql/allowed_acl.json", _=gettext),
+ status=200,
+ mimetype="application/javascript")
+
+
+@blueprint.route(
+ '/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
+ """
+
+ # unquote encoded url parameter
+ node_type = unquote(node_type)
+ nspname = unquote(nspname)
+
+ res_data = []
+ manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
+ conn = manager.connection(did=did)
+
+ # 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', 'view']:
+ SQL = render_template("/".join(
+ [template_path, 'properties.sql']),
+ node_id=node_id, node_name=node_name,
+ type='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
+ )
+
+
+def parse_priv_to_db(str_privileges, acls):
+ """
+ Common utility function to parse privileges before sending to database.
+ """
+ db_privileges = {
+ 'c': 'CONNECT',
+ 'C': 'CREATE',
+ 'T': 'TEMPORARY',
+ 'a': 'INSERT',
+ 'r': 'SELECT',
+ 'w': 'UPDATE',
+ 'd': 'DELETE',
+ 'D': 'TRUNCATE',
+ 'x': 'REFERENCES',
+ 't': 'TRIGGER',
+ 'U': 'USAGE',
+ 'X': 'EXECUTE'
+ }
+
+ privileges = []
+
+ for priv in str_privileges:
+ priv_with_grant = {}
+ priv_without_grant = {}
+ priv_function_with_grant = []
+ priv_function_without_grant = []
+ priv_sequence_with_grant = []
+ priv_sequence_without_grant = []
+ priv_table_with_grant = []
+ priv_table_without_grant = []
+
+ # import pdb
+ # pdb.set_trace()
+ for privilege in priv['privileges']:
+
+ if privilege['privilege_type'] not in db_privileges:
+ continue
+
+ # Extract privileges applicable to function
+ if privilege['privilege_type'] in acls['function']['acl']:
+ if privilege['with_grant']:
+ priv_function_with_grant.append(
+ db_privileges[privilege['privilege_type']])
+ elif privilege['privilege']:
+ priv_function_without_grant.append(
+ db_privileges[privilege['privilege_type']])
+
+ # Extract privileges applicable to sequence
+ if privilege['privilege_type'] in acls['sequence']['acl']:
+ if privilege['with_grant']:
+ priv_sequence_with_grant.append(
+ db_privileges[privilege['privilege_type']])
+ elif privilege['privilege']:
+ priv_sequence_without_grant.append(
+ db_privileges[privilege['privilege_type']])
+
+ # Extract privileges applicable to view/table
+ if (privilege['privilege_type'] in acls['table']['acl']):
+ if privilege['with_grant']:
+ priv_table_with_grant.append(
+ db_privileges[privilege['privilege_type']])
+ elif privilege['privilege']:
+ priv_table_without_grant.append(
+ db_privileges[privilege['privilege_type']])
+
+ # If we have all acl then just return all
+ if len(priv_table_with_grant) == len(acls['table']['acl']):
+ priv_table_with_grant = ['ALL']
+
+ if len(priv_table_without_grant) == len(acls['table']['acl']):
+ priv_table_without_grant = ['ALL']
+
+ # Appending and returning all ACL
+ priv_with_grant['function'] = priv_function_with_grant
+ priv_without_grant['function'] = priv_function_without_grant
+
+ priv_with_grant['sequence'] = priv_sequence_with_grant
+ priv_without_grant['sequence'] = priv_sequence_without_grant
+
+ priv_with_grant['table'] = priv_table_with_grant
+ priv_without_grant['table'] = priv_table_without_grant
+
+ privileges.append({
+ 'grantee': priv['grantee'],
+ 'with_grant': priv_with_grant,
+ 'without_grant': priv_without_grant
+ })
+
+ return privileges
+
+
+@blueprint.route(
+ '/msql//////',
+ methods=('GET', 'POST'))
+def msql(gid, sid, did, node_type, nspname):
+ """
+ This function will return modified SQL
+ """
+
+ # unquote encoded url parameter
+ node_type = unquote(node_type)
+ nspname = unquote(nspname)
+
+ 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)
+
+ 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)
+
+ # 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(
+ '/save////'
+ '//',
+ methods=('GET', 'POST'))
+def save(gid, sid, did, node_type, nspname):
+ """
+ This function will apply the privileges to the selected
+ Database Objects
+ """
+
+ # unquote encoded url parameter
+ node_type = unquote(node_type)
+ nspname = unquote(nspname)
+
+ 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)
+
+ 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)
+
+ # Pass database objects and get SQL for privileges
+ SQL = render_template("/".join(
+ [template_path, 'grant.sql']),
+ data=data, nspname=nspname, conn=conn)
+
+ 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 internal_server_error(errormsg=e.message)
diff --git a/web/pgadmin/tools/grant_wizard/static/css/grant_wizard.css b/web/pgadmin/tools/grant_wizard/static/css/grant_wizard.css
new file mode 100644
index 0000000..46c9203
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/static/css/grant_wizard.css
@@ -0,0 +1,86 @@
+/** 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 {
+ display: inline-block;
+ height: 100%;
+ border: 0 !important;
+}
+
+.object_type_table tbody {
+ display: block;
+ overflow: scroll;
+ border: 1px solid #ddd;
+ width: 100%;
+ min-height: 100%;
+ max-height: 66%;
+ height: 66%;
+}
+
+.object_type_table tbody tr td {
+ background-position: 4px 4px;
+ border-radius: 0;
+}
+
+.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 thead tr th:nth-child(4) {
+ width: 244px;
+}
+
+.object_type_table tbody tr td:nth-child(4) {
+ width: 244px;
+ min-width: 228px;
+ max-width: 100%;
+}
+
+/** Override Backgrid filter CSS **/
+.backgrid-filter.form-search {
+ float: left;
+ margin: 0px 5px 5px 0;
+}
+
+/** Custom styling for Codemirror field **/
+.wizard-right-panel_content {
+ border: 1px solide #ccc;
+}
+
+.wizard-right-panel_content .CodeMirror {
+ border: 1px solid #ccc;
+ height: 285px !important;
+ min-height: 285px !important;
+}
+
+.wizard-right-panel_content .CodeMirror-linenumber {
+ background: #F7F7F7;
+ border-right: 1px solid #DDDDDD;
+}
+
+.wizard-right-panel_content .CodeMirror-gutters {
+ min-height: 285px !important;
+}
diff --git a/web/pgadmin/tools/grant_wizard/templates/grant_wizard/js/grant_wizard.js b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/js/grant_wizard.js
new file mode 100644
index 0000000..54805b1
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/js/grant_wizard.js
@@ -0,0 +1,1091 @@
+define([
+ 'jquery', 'underscore', 'underscore.string', 'alertify',
+ 'pgadmin.browser', 'backbone', 'backgrid', 'pgadmin.browser.node',
+ 'backgrid.select.all', 'backgrid.filter', 'pgadmin.browser.server.privilege',
+ 'pgadmin.browser.wizard',
+ ],
+
+ // This defines Grant Wizard dialog
+ function($, _, S, alertify, pgBrowser, Backbone, Backgrid, pgNode) {
+
+ // if module is already initialized, refer to that.
+ if (pgBrowser.GrantWizard) {
+ return pgBrowser.GrantWizard;
+ }
+
+ /**
+ It is sub model for field "Objects". It has fields
+ for database object types such as Schemas, Views and
+ Sequence etc.
+ */
+ var DatabaseObjectModel = pgNode.Model.extend({
+ defaults: {
+ selected: false,
+ icon: 'icon-unknown',
+ name: undefined,
+ name_with_args: undefined,
+ nspname: undefined,
+ proargs: undefined,
+ object_type: undefined,
+ object_id: undefined
+ },
+ idAttribute: 'object_id', // to uniquely identify a model object
+ toJSON: function(obj) {
+ var d = pgNode.Model.prototype.toJSON.apply(this);
+ delete d.icon;
+ return d;
+ },
+ parse: function(res) {
+
+ // Create unique object id
+ res.object_id = res.name_with_args;
+
+ // create name with args if its object is function
+ if(!_.isUndefined(res.object_type) && (res.object_type == 'Function' ||
+ res.object_type == 'Trigger Function'))
+ res.name_with_args = res.name+'('+(typeof(res.proargs) != 'undefined' ? res.proargs : '')+')';
+ else
+ res.name_with_args = res.name;
+
+ return res;
+ },
+
+ validate: function() {
+
+ /*
+ * Triggers error messages for object types "selected"
+ * if it is empty/undefined/null
+ */
+ var err = {},
+ errmsg,
+ node = this.get('objects').toJSON();
+ if (_.isEmpty(node)) {
+ err['selected'] = '{{ _("Please select any database object type") }}';
+ errmsg = errmsg || err['selected'];
+ this.errorModel.set('selected', errmsg);
+ return errmsg;
+ } else {
+ this.errorModel.unset('selected');
+ }
+ return null;
+ }
+ });
+
+ // Define columns for the Db Object Types grid
+ var columns = [{
+ name: "selected",
+
+ /*
+ Override render method of Backgrid.Extension.SelectRowCell
+ class. It has an issue: It doesn't mark rows checked if we move to next
+ page and then go back to previous page. but it must show.
+ so we handle this case by overriding the render method.
+ */
+ cell: Backgrid.Extension.SelectRowCell.extend({
+ render: function() {
+
+ // Use the Backform Control's render function
+ Backgrid.Extension.SelectRowCell.prototype.render.apply(this, arguments);
+
+ var col = this.column.get('name');
+ if (this.model && this.model.has(col)) {
+ if (this.model.get(col)) {
+ this.checkbox().prop("checked", true);
+ this.$el.parent().toggleClass("selected", true);
+ this.model.trigger("backgrid:selected", this.model, true);
+ }
+ }
+ return this;
+ }
+ }),
+
+ headerCell: "select-all",
+
+ },{
+ name: "object_type",
+ label: "Object Type",
+ editable: false,
+ cell: Backgrid.Cell.extend({
+ render: function() {
+
+ // Override render to add icon to Db Object column
+ Backgrid.Cell.prototype.render.apply(this, arguments);
+ this.$el.addClass(this.model.get('icon')).css({"padding-left": "24px"});
+
+ return this;
+ }
+ })
+ },{
+ name: "nspname",
+ label: "Schema",
+ cell: "string",
+ editable: false
+ },{
+ name: "name_with_args",
+ label: "Name",
+ cell: "string",
+ editable: false
+ }];
+
+ // Create an Object GrantWizard of pgBrowser class
+ pgBrowser.GrantWizard = {
+ init: function() {
+ if (this.initialized)
+ return;
+
+ this.initialized = true;
+
+ // Define list of nodes on which grant wizard context menu option appears
+ var supported_nodes = [
+ 'schema', 'coll-function',
+ 'coll-sequence', 'coll-table',
+ 'coll-view', 'coll-materialized_view'
+ ],
+
+ /**
+ Enable/disable grantwizard menu in tools based
+ on node selected
+ if selected node is present in supported_nodes,
+ menu will be enabled otherwise disabled.
+ Also, hide it for system view in catalogs
+ */
+ menu_enabled = function(itemData, item, data) {
+ var t = pgBrowser.tree, i = item, d = itemData;
+ var parent_item = t.hasParent(i) ? t.parent(i): null,
+ parent_data = parent_item ? t.itemData(parent_item) : null;
+ if(!_.isUndefined(d) && !_.isNull(d) && !_.isNull(parent_data))
+ return ((_.indexOf(supported_nodes, d._type) !== -1 && parent_data._type != 'catalog') ? true: false);
+ else
+ return false;
+ };
+
+ // Define the nodes on which the menus to be appear
+ var menus = [{
+ name: 'grant_wizard_schema', module: this,
+ applies: ['tools'], callback: 'start_grant_wizard',
+ priority: 10, label: '{{_("Grant Wizard...") }}',
+ icon: 'wcTabIcon', enable: menu_enabled
+ }];
+
+ // Add supported menus into the menus list
+ for (var idx = 0; idx < supported_nodes.length; idx++) {
+ menus.push({
+ name: 'grant_wizard_schema_context_' + supported_nodes[idx],
+ node: supported_nodes[idx], module: this,
+ applies: ['context'], callback: 'start_grant_wizard',
+ priority: 10, label: '{{_("Grant Wizard...") }}',
+ icon: 'wcTabIcon', enable: menu_enabled
+ });
+ }
+ pgAdmin.Browser.add_menus(menus);
+
+ return this;
+ },
+
+ // Callback to draw Wizard Dialog
+ start_grant_wizard: function(action, item) {
+
+ // Declare Wizard dialog
+ if (!alertify.wizardDialog) {
+ alertify.dialog('wizardDialog', function factory() {
+
+ // Generate wizard main container
+ var $container = $("");
+
+ return {
+ main: function(title) {
+ this.set('title', title);
+ },
+ setup:function() {
+ return {
+
+ // Set options for dialog
+ options: {
+ frameless: true,
+ resizable: false,
+ autoReset: false,
+ maximizable: false,
+ closableByDimmer: false
+ }
+ };
+ },
+ hooks:{
+ onshow: function() {
+
+ // Set dimensions for wizard
+ this.elements.dialog.style.width = '100%';
+ this.elements.dialog.style.height = '68%';
+
+ // Add pgadmin_grant_wizard_body class to dialog
+ $(this.elements.body).addClass('pgadmin_grant_wizard_body');
+ },
+ },
+
+ /**
+ Returns a Paginator Class Object which is again to be rendered
+
+ @class {Backgrid.Extension.Paginator}
+ @param {Backbone.Collection} coll - from which data is fetched
+ @return {Object} paginator
+ */
+ DbPaginator: function(coll){
+ var paginator = this.paginator = new Backgrid.Extension.Paginator({
+ collection: coll,
+ windowSize: 8
+ });
+ return paginator;
+ },
+
+ /**
+ Create new Filter which will filter the
+ rendered grid for Select Type Tabular Data
+ @param {Backbone.PageableCollection} coll
+ */
+ DbObjectFilter: function(coll){
+ var clientSideFilter = this.clientSideFilter = new Backgrid.Extension.ClientSideFilter({
+ collection: coll,
+ placeholder: _('Search by object type or name'),
+
+ // The model fields to search for matches
+ fields: ['object_type', 'name'],
+
+ // How long to wait after typing has stopped before searching can start
+ wait: 150
+ });
+ return clientSideFilter;
+ },
+
+ //Enable Disable Next button of PrivilegePage
+ updateButtons: function(modified){
+ if(!modified)
+ $('.wizard-next').prop('disabled', true);
+ else
+ $('.wizard-next').prop('disabled', false);
+ },
+
+ /**
+ Callback called when an errorModel is set
+ with invalid value and errormsg is set into
+ status bar element and next button is disabled
+ */
+ onSessionInvalid: function(msg) {
+ $('.error_msg_div p').html(msg).removeClass("hide");
+
+ // Enable disable Next button
+ this.updateButtons(false);
+ return true;
+ },
+
+ /**
+ Callback called when anything is set into model
+ thus hide error msg element and enable next button
+ status bar element and next button is disabled
+ */
+ onSessionValidated: function(sessHasChanged) {
+ $('.error_msg_div p').empty().addClass("hide");
+
+ // Enable disable Next button
+ this.updateButtons(sessHasChanged);
+ },
+
+ /**
+ Remove/Delete objects, attributes
+ in wizard on wizard close or finish
+ to reclaim memory
+ */
+ releaseObjects: function(){
+ var self = this;
+
+ if(!_.isUndefined(self.dbObjectFilter)) {
+ self.dbObjectFilter.remove();
+ self.dbObjectFilter = undefined;
+ }
+
+ if(!_.isUndefined(self.clientSideFilter)) {
+ self.clientSideFilter.remove();
+ self.clientSideFilter = undefined;
+ }
+
+ // clear object priv array
+ if(!_.isNull(self.obj_priv) &&
+ !_.isUndefined(self.obj_priv)) {
+ self.obj_priv = [];
+ delete self.obj_priv;
+ }
+
+ // Delete Wizard Pages, clear model and cleanup view
+ if(!_.isUndefined(self.dbObjectTypePage) &&
+ !_.isNull(self.dbObjectTypePage)) {
+ if(!_.isUndefined(self.dbObjectTypePage.get('model')) &&
+ !_.isNull(self.dbObjectTypePage.get('model'))) {
+ self.dbObjectTypePage.get('model').clear();
+ self.dbObjectTypePage.get('view').cleanup();
+ self.dbObjectTypePage = undefined;
+ }
+ }
+
+ if(!_.isUndefined(self.privilegePage) &&
+ !_.isNull(self.privilegePage)) {
+ if(!_.isUndefined(self.privilegePage.get('model')) &&
+ !_.isNull(self.privilegePage.get('model'))) {
+ self.privilegePage.get('model').clear();
+ self.privilegePage.get('view').cleanup();
+ self.privilegePage = undefined;
+ }
+ }
+
+ if(!_.isUndefined(self.reviewSQLPage) &&
+ !_.isNull(self.reviewSQLPage)) {
+ if(!_.isUndefined(self.reviewSQLPage.get('model')) &&
+ !_.isNull(self.reviewSQLPage.get('model'))) {
+ self.reviewSQLPage.get('model').clear();
+ self.reviewSQLPage = undefined;
+ }
+ }
+
+ // Remove Sql control
+ if (!_.isUndefined(self.sqlControl)) {
+ self.sqlControl.remove();
+ }
+
+ // Clear privModel
+ if(!_.isNull(self.privModel) &&
+ !_.isUndefined(self.privModel)) {
+ self.privModel.clear();
+ }
+
+ // Remove collection containing db object data
+ if(!_.isNull(self.coll) &&
+ !_.isUndefined(self.coll)) {
+ self.coll.reset();
+ self.coll = undefined;
+ }
+ // Delete Wizard
+ if(!_.isNull(self.wizard) &&
+ !_.isUndefined(self.wizard)) {
+ self.wizard.collection.reset();
+ self.wizard.curr_page = undefined;
+ }
+
+ },
+
+ /**
+ Every time a wizard is opened, this function
+ is called everytime. It has Wizard Pages which
+ are rendered by the Wizard Class:
+
+ @class {pgBrowser.WizardPage} dbObjectType1 - This page
+ @extends {Backbone.Model}
+ renders a grid of Database Object Types such as
+ Schemas, Views and Sequences etc.
+
+ @class {pgBrowser.WizardPage} WizardPage2 - This page
+ @extends {Backbone.Model}
+ adds Privilege Control which provides grant privileges
+ such as "Create, Insert, Delete, Update" so on the
+ database objects selected on Wizard Pages.
+
+ @class {pgBrowser.WizardPage} WizardPage3 - This page
+ displays the generated GRANT SQL query for the Db
+ objects selected with the specific privileges added to it.
+ @extends {Backbone.Model}
+
+ @class {Backbone.Collection} WizardCollection - It is the
+ collection of wizard pages
+
+ @class {pgBrowser.Wizard} wizard - Its task is:
+ - Create a Wizard
+ - Add Buttons, Callbacks to it.
+ - Render WizardPages
+ @extends {Backbone.View}
+
+ */
+ build: function() {
+ this.elements.content.appendChild($container.get(0));
+ },
+
+ //Returns list of Acls defined for nodes
+ get_json_data: function() {
+ var url = "/grant_wizard/allowed_acl.json";
+ return $.ajax({
+ async: false,
+ dataType: 'jsonp',
+ url: url
+ });
+
+ },
+ prepare:function() {
+
+ $container.empty().append("");
+
+ // Define el for wizard view
+ var el = $('.grant_wizard_container');
+
+ // Fetch privileges specific to nodes
+ var json_data = this.get_json_data(),
+ privDict = JSON.parse(json_data.responseText);
+
+ // Extract the data from the selected tree node
+ var t = pgBrowser.tree,
+ i = t.selected(),
+ d = this.d = i && i.length == 1 ? t.itemData(i) : undefined,
+ info = this.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,
+
+ /**
+ get node name only. used in mapping with object types defined
+ in allowed_acl.json
+ */
+ node_type = d._type.replace('coll-', '').replace('materialized_', ''),
+ node_label = d.label;
+
+ // Collection url to fetch database object types for objects field
+ var baseUrl = "{{ url_for('grant_wizard.index') }}" + "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('grant_wizard.index') }}" + "save/" +
+ S('%s/%s/%s/%s/%s/').sprintf(
+ encodeURI(gid), encodeURI(sid), encodeURI(did),
+ encodeURI(node_type), encodeURI(nspname)).value(),
+
+ // generate encoded url based on wizard type
+ msql_url = this.msql_url = "/grant_wizard/msql/"+
+ S('%s/%s/%s/%s/%s/').sprintf(
+ encodeURI(gid), encodeURI(sid), encodeURI(did),
+ encodeURI(node_type), encodeURI(nspname)).value(),
+
+ Coll = Backbone.Collection.extend({
+ model: DatabaseObjectModel,
+ url: baseUrl
+ }),
+
+ // Create instances of collection and filter
+ coll = this.coll = new Coll(),
+ dbObjectFilter = this.dbObjectFilter = this.DbObjectFilter(coll);
+
+ /**
+ privArray holds objects selected which further helps
+ in creating privileges Model
+ */
+ var self = this;
+ self.privArray = [];
+
+ /**
+ Override backgrid listener "backgrid:selected" to
+ Add/Remove model to/from objects collection
+ */
+ coll.on('backgrid:selected', function(model, selected) {
+ model.set('selected', selected);
+
+ var object_type = model.get('object_type');
+ switch (object_type)
+ {
+ case 'Function':
+ object_type = 'function';
+ break;
+ case 'Trigger Function':
+ object_type = 'function';
+ break;
+ case 'Table':
+ object_type = 'table';
+ break;
+ case 'Sequence':
+ object_type = 'sequence';
+ break;
+ case 'View':
+ object_type = 'table';
+ break;
+ }
+
+ /**
+ if a row (checkbox) is checked, add that model
+ into collection, when unchecked remove it from
+ model.
+
+ Also push/pop object type in/from privArray
+ */
+ if(selected) {
+ if(_.indexOf(self.privArray, object_type) == -1)
+ self.privArray.push(object_type);
+ newModel.get('objects').add(model, { silent: true });
+ }
+ else {
+ var idx = self.privArray.indexOf(object_type);
+ if(idx !=-1)
+ self.privArray.splice(idx, 1);
+ 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);
+ });
+
+ /**
+ 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") }}',
+ model: pgAdmin.Browser.Node.PrivilegeRoleModel,
+ type: 'collection', canAdd: true,
+ canDelete: true, control: 'unique-col-collection'
+ }
+ ],
+ urlRoot: saveUrl
+ });
+
+ /**
+ Create instance of GrantWizard Model, provide urlRoot
+ node_info object, Generate fields objects
+ */
+ var newModel = new GrantWizardModel({}, { node_info: info });
+
+ /**
+ 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);
+
+ //////////////////////////////////////////////////////////////////////
+ // //
+ // 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_description: _('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: 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);
+
+ // 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;
+ obj.options.disable_next = true;
+
+ /**
+ Enable/Disable next button of privilegePage if objects
+ are present in model
+ */
+ if(!_.isNull(newModel.get('acl')) &&
+ !_.isUndefined(newModel.get('acl'))) {
+ 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 //
+ // //
+ //////////////////////////////////////////////////////////////////////
+
+ // Wizard for Privelege control
+ var privilegePage = self.privilegePage = new pgBrowser.WizardPage({
+ id: 2,
+ page_title: _('Privileges Selection (step 2 of 3)'),
+ show_description: _('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() {
+
+ var obj_priv = [];
+ self.privArray = _.uniq(self.privArray);
+ _.each(self.privArray, function(priv){
+ self.obj_priv = obj_priv = _.union(obj_priv , privDict[priv].acl);
+ });
+
+ /**
+ Define PrivModel and its instance.
+ Privileges array is generated based on
+ the type of nodes selected.
+ */
+ var privModel = self.privModel;
+ var PrivModel = pgNode.Model.extend({
+ defaults: {
+ acl: undefined
+ },
+ schema: [
+ {
+ id: 'acl', label: '{{ _("Privileges") }}',
+ model: pgAdmin.Browser.Node.PrivilegeRoleModel.extend({
+
+ // privileges are selected based on node clicked
+ privileges: obj_priv
+ }), uniqueCol : ['grantee', 'grantor'], editable: true,
+ type: 'collection', canAdd: true,
+ canDelete: true, control: 'unique-col-collection'
+ }
+ ]
+ });
+
+ /**
+ When privelege control is re-rendered, in order to
+ render privileges based on object type selected,
+ delete privileges from privModel which are now not
+ present in object privileges array(object_priv)
+ */
+ var data = {};
+ if (privModel) {
+ data = privModel.toJSON();
+ var rolePrivs = data['acl'];
+
+ for (var idx in rolePrivs) {
+ var rolePriv = (rolePrivs[idx])['privileges'],
+ removeIdx = [], j;
+
+ for (j in rolePriv) {
+ var p = rolePriv[j];
+ if (_.indexOf(obj_priv, p['privilege_type']) == -1) {
+ removeIdx.push(j);
+ }
+ }
+
+ for (j in removeIdx) {
+ rolePriv.splice(j, 1);
+ }
+ }
+ }
+
+ // Instantiate privModel
+ privModel = self.privModel = new PrivModel(data, { node_info: self.info });
+
+ /*
+ To track changes into model, start new session
+ and Add event listener for privileges control
+ */
+ self.privModel.startNewSession();
+ self.privModel.on('pgadmin-session:valid', self.onSessionValidated.bind(self));
+ self.privModel.on('pgadmin-session:invalid', self.onSessionInvalid.bind(self));
+
+ /**
+ Create Field Object which has properties like
+ node_data, node_info which is required for rendering
+ Privilege control
+ */
+ var fields = Backform.generateViewSchema(
+ self.info, self.privModel, 'create', self.d._type, self.d
+ );
+ var privilegesField = new Backform.Field(fields[0].fields[0]);
+
+ this.privControl = new (privilegesField.get('control')) ({
+ field: privilegesField,
+ model: self.privModel
+ });
+
+ 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 DbObjectType page 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 Filter
+ */
+ newModel.trigger("backgrid:refresh", newModel, false);
+ self.clientSideFilter.render();
+ return true;
+ },
+
+ beforeNext: function() { return true; },
+
+ onNext: function(obj){
+
+ // Assign acls of privModel to main model newModel
+ if (!_.isUndefined(self.privModel)) {
+ newModel.set({'acl': self.privModel.get('acl')});
+ }
+
+ // Remove the privilege control
+ if (this.view) {
+ this.view.cleanup();
+ delete this.view;
+ this.view = null;
+ }
+
+ // Enable finish button
+ self.wizard.options.disable_finish = false;
+
+ /**
+ triggers to get SQL queries data to render
+ into the reviewSQLPage
+ */
+ newModel.trigger('pgadmin-wizard:nextpage:sql', {'node_type': node_type });
+ }
+ });
+
+
+ //////////////////////////////////////////////////////////////////////
+ // //
+ // Review SQL Query Page //
+ // //
+ //////////////////////////////////////////////////////////////////////
+
+ //Create SqlField Object
+ var sqlField = new Backform.Field(
+ {
+ id: 'sqltab',
+ label: _('Sql Tab'),
+
+ /**
+ Extend 'SqlTabControl' to define new
+ function 'onWizardNextPageChange'
+ which gets triggered on next button
+ click to fetch generated SQL query
+ for the selected db objects.
+ */
+ control: Backform.SqlTabControl.extend({
+ initialize: function() {
+
+ // Initialize parent class
+ Backform.SqlTabControl.prototype.initialize.apply(this, arguments);
+
+ this.msql_url = self.msql_url;
+
+ // define trigger events for prev and next page
+ this.model.on('pgadmin-wizard:nextpage:sql', this.onWizardNextPageChange, this);
+ this.model.on('pgadmin-wizard:prevpage:sql', this.onWizardPrevPageChange, this);
+ },
+
+ // This method fetches the modified SQL for the wizard
+ onWizardNextPageChange: function(){
+
+ var self = this;
+
+ // Fetches modified SQL
+ $.ajax({
+ url: this.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() {
+
+ // Clear html dom elements of CodeMirror sql tab
+ self.sqlControl.unbind(); // Unbind all local event bindings
+ var cmElem = self.sqlControl.sqlTab.getWrapperElement();
+ cmElem.remove();
+ self.sqlControl.sqlTab = undefined;
+ }
+
+ })
+ }),
+
+ /**
+ Create sqlField view instance
+ to render it into wizard page
+ */
+ sqlControl = self.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_description: _('Following query will be executed on the database ' +
+ 'server for the selected objects, and privileges. ' +
+ 'Please click on Finish to complete the process.'),
+ model: newModel,
+ 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 if privilege
+ model is not empty else disable
+ next button
+ */
+ var aclModel = newModel.get('acl');
+
+ if(!_.isUndefined(wizardObj.collection) &&
+ wizardObj.collection.models.length > 0) {
+ 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
+ */
+ 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
+ },
+
+ // Callback for finish button
+ onFinish: function() {
+ var m = newModel,
+ d = m.toJSON('GET');
+
+ // Save model
+ if (d && !_.isEmpty(d) && !_.isUndefined(d.objects)) {
+ m.save({}, {
+ attrs: d,
+ validate: false,
+ cache: false,
+ success: function(res) {
+
+ // Release wizard objects
+ self.releaseObjects();
+ self.close();
+ },
+ error: function(m, jqxhr) {
+ alertify.pgNotifier(
+ "error", jqxhr,
+ S(
+ "{{ _('Error during saving properties - %%s!') }}"
+ ).sprintf(jqxhr.statusText).value()
+ );
+ }
+ });
+ }
+ },
+
+ // Callback for cancel button
+ onCancel: function() {
+
+ // Release wizard objects
+ self.releaseObjects();
+ self.close();
+ }
+ })) ({
+ collection: this.wizardCollection,
+ el: el,
+ model: newModel
+ });
+
+ // Render wizard
+ self.wizard.render();
+ }
+ };
+ });
+ }
+
+ // Call Grant Wizard Dialog
+ alertify.wizardDialog(true);
+ }
+ };
+
+ return pgBrowser.GrantWizard;
+ });
diff --git a/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/allowed_acl.json b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/allowed_acl.json
new file mode 100644
index 0000000..2d21f14
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/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"]
+ },
+ "table": {
+ "type": "TABLE",
+ "acl": ["a", "w", "d", "D", "x", "t"]
+ },
+ "view": {
+ "type": "VIEW",
+ "acl": ["a", "w", "d", "D", "x", "t"]
+ },
+ "sequence": {
+ "type": "SEQUENCE",
+ "acl": ["w", "U"]
+ },
+ "function": {
+ "type": "FUNCTION",
+ "acl": ["X"]
+ }
+}
diff --git a/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/grant.sql b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/grant.sql
new file mode 100644
index 0000000..ae59b53
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/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|upper -%}
+{# ===== if object_type is Function then apply function marcros ===== #}
+{% if (obj_type == 'FUNCTION' or obj_type == 'TRIGGER FUNCTION') and (priv.with_grant['function']|length > 0 or priv.without_grant['function']|length > 0) -%}
+{{ PRIVILEGE_FUNCTION.SET(conn, 'FUNCTION', priv.grantee, obj.name, priv.without_grant['function'], priv.with_grant['function'], nspname, obj.proargs)}}
+{% elif (obj_type == 'SEQUENCE') and (priv.with_grant['table']|length > 0 or priv.without_grant['table']|length > 0) -%}
+{{ PRIVILEGE.SET(conn, obj_type.upper(), priv.grantee, obj.name, priv.without_grant['sequence'], priv.with_grant['sequence'], nspname ) }}
+{% elif (obj_type == 'VIEW' or obj_type == 'TABLE') and (priv.with_grant['table']|length > 0 or priv.without_grant['table']|length > 0) -%} {# ===== Views are also table ===== #}
+{%- set obj_type = 'TABLE' -%}
+{{ PRIVILEGE.SET(conn, obj_type.upper(), priv.grantee, obj.name, priv.without_grant['table'], priv.with_grant['table'], nspname ) }}
+{% endif -%}
+{% endfor -%}
+{% endfor -%}
+{% endif -%}
diff --git a/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/properties.sql b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/properties.sql
new file mode 100644
index 0000000..e92888c
--- /dev/null
+++ b/web/pgadmin/tools/grant_wizard/templates/grant_wizard/sql/properties.sql
@@ -0,0 +1,66 @@
+{#===================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
+ pg_get_function_identity_arguments(pr.oid) AS proargs,
+ {# pr.proname || '(' || pg_get_function_identity_arguments(pr.oid) || ')' AS name,#}
+ pr.proname AS name,
+ '{{ func_type }}' AS object_type,
+ '{{ nspname }}' AS nspname,
+ '{{ "icon-function" if type != "trigger_function" else "icon-trigger_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 == 'view' and node_id and node_name %}
+SELECT
+ c.relname AS name,
+ 'View' AS object_type,
+ '{{ nspname }}' AS nspname,
+ 'icon-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 %}