From be7bb81a194aef84b3c685f41290659ef1d85af6 Mon Sep 17 00:00:00 2001 From: Nikhil Mohite Date: Thu, 1 Oct 2020 13:29:00 +0530 Subject: [PATCH] Allow user to change the database connection from an open query tool tab. Fixes #3794 --- docs/en_US/images/new_connection_dialog.png | Bin 0 -> 49691 bytes docs/en_US/images/new_connection_options.png | Bin 0 -> 45739 bytes docs/en_US/query_tool.rst | 21 ++ docs/en_US/release_notes_4_27.rst | 1 + .../browser/server_groups/servers/__init__.py | 10 +- .../servers/roles/tests/utils.py | 35 ++ web/pgadmin/model/__init__.py | 47 +++ .../js/sqleditor/new_connection_dialog.js | 262 ++++++++++++++ .../sqleditor/new_connection_dialog_model.js | 339 ++++++++++++++++++ web/pgadmin/static/scss/_alert.scss | 1 + web/pgadmin/tools/datagrid/__init__.py | 146 +++++++- .../datagrid/templates/datagrid/index.html | 14 +- web/pgadmin/tools/datagrid/tests/__init__.py | 0 .../datagrid/tests/datagrid_test_data.json | 134 +++++++ .../tests/test_data_grid_init_query_tool.py | 73 ++++ .../datagrid/tests/test_data_grid_panel.py | 90 +++++ .../tests/test_data_grid_query_tool_close.py | 78 ++++ .../tests/test_data_grid_update_connection.py | 121 +++++++ .../tests/test_data_grid_validate_filter.py | 92 +++++ .../tests/test_initialize_data_grid.py | 109 ++++++ web/pgadmin/tools/datagrid/tests/utils.py | 33 ++ web/pgadmin/tools/sqleditor/__init__.py | 336 +++++++++++++++-- .../tools/sqleditor/static/css/sqleditor.css | 9 +- .../tools/sqleditor/static/js/sqleditor.js | 154 +++++++- .../sqleditor/static/scss/_sqleditor.scss | 13 + .../tests/test_new_connection_database.py | 100 ++++++ .../tests/test_new_connection_dialog.py | 50 +++ .../tests/test_new_connection_user.py | 100 ++++++ web/pgadmin/utils/constants.py | 2 + .../utils/driver/psycopg2/connection.py | 113 ++++-- 30 files changed, 2394 insertions(+), 89 deletions(-) create mode 100644 docs/en_US/images/new_connection_dialog.png create mode 100644 docs/en_US/images/new_connection_options.png create mode 100644 web/pgadmin/static/js/sqleditor/new_connection_dialog.js create mode 100644 web/pgadmin/static/js/sqleditor/new_connection_dialog_model.js create mode 100644 web/pgadmin/tools/datagrid/tests/__init__.py create mode 100644 web/pgadmin/tools/datagrid/tests/datagrid_test_data.json create mode 100644 web/pgadmin/tools/datagrid/tests/test_data_grid_init_query_tool.py create mode 100644 web/pgadmin/tools/datagrid/tests/test_data_grid_panel.py create mode 100644 web/pgadmin/tools/datagrid/tests/test_data_grid_query_tool_close.py create mode 100644 web/pgadmin/tools/datagrid/tests/test_data_grid_update_connection.py create mode 100644 web/pgadmin/tools/datagrid/tests/test_data_grid_validate_filter.py create mode 100644 web/pgadmin/tools/datagrid/tests/test_initialize_data_grid.py create mode 100644 web/pgadmin/tools/datagrid/tests/utils.py create mode 100644 web/pgadmin/tools/sqleditor/tests/test_new_connection_database.py create mode 100644 web/pgadmin/tools/sqleditor/tests/test_new_connection_dialog.py create mode 100644 web/pgadmin/tools/sqleditor/tests/test_new_connection_user.py diff --git a/docs/en_US/images/new_connection_dialog.png b/docs/en_US/images/new_connection_dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..8cf7ae6aa750360ae7710af61bf8e89f9d520284 GIT binary patch literal 49691 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 %0^c>O%*OkVmcSQwgzI6WP2m3$pEbze=t& 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 NIVmN{VsYb-{|8bOF{}Up literal 0 HcmV?d00001 diff --git a/docs/en_US/images/new_connection_options.png b/docs/en_US/images/new_connection_options.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca94374baf0eb9a8a0e49cbd9d4d426895ff9d0 GIT binary patch literal 45739 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 literal 0 HcmV?d00001 diff --git a/docs/en_US/query_tool.rst b/docs/en_US/query_tool.rst index 83933f0ad..80f27a5dc 100644 --- a/docs/en_US/query_tool.rst +++ b/docs/en_US/query_tool.rst @@ -300,3 +300,24 @@ transaction status by clicking on the status icon in the Query Tool: .. image:: images/query_tool_connection_status.png :alt: Query tool connection and transaction statuses :align: center + +Change connection +***************** + +User can connect to another server or database from existing open session of query tool. + +* Click on the connection link next to connection status. +* Now click on the ** option from the dropdown. + +.. image:: images/new_connection_options.png + :alt: Query tool connection options + :align: center + +* Now select server, database, user, and role to connect and click OK. + +.. image:: images/new_connection_dialog.png + :alt: Query tool connection dialog + :align: center + +* A newly created connection will now get listed in the options. +* To connect, select the newly created connection from the dropdown list. diff --git a/docs/en_US/release_notes_4_27.rst b/docs/en_US/release_notes_4_27.rst index 37cee08c4..8bcbf7d76 100644 --- a/docs/en_US/release_notes_4_27.rst +++ b/docs/en_US/release_notes_4_27.rst @@ -10,6 +10,7 @@ New features ************ | `Issue #1402 `_ - Added Macro support. +| `Issue #3794 `_ - Allow user to change the database connection from an open query tool tab. | `Issue #5200 `_ - Added support to ignore the owner while comparing objects in the Schema Diff tool Housekeeping diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index f22e2e6d9..2630d1e2e 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -1247,7 +1247,7 @@ class ServerNode(PGChildNodeView): } ) - def connect(self, gid, sid): + def connect(self, gid, sid, user_name=None): """ Connect the Server and return the connection object. Verification Process before Connection: @@ -1368,7 +1368,8 @@ class ServerNode(PGChildNodeView): # not provided, or password has not been saved earlier. if prompt_password or prompt_tunnel_password: return self.get_response_for_password(server, 428, prompt_password, - prompt_tunnel_password) + prompt_tunnel_password, + user=user_name) status = True try: @@ -1802,7 +1803,8 @@ class ServerNode(PGChildNodeView): return internal_server_error(errormsg=str(e)) def get_response_for_password(self, server, status, prompt_password=False, - prompt_tunnel_password=False, errmsg=None): + prompt_tunnel_password=False, errmsg=None, + user=None): if server.use_ssh_tunnel: return make_json_response( @@ -1829,7 +1831,7 @@ class ServerNode(PGChildNodeView): result=render_template( 'servers/password.html', server_label=server.name, - username=server.username, + username=user if user else server.username, errmsg=errmsg, service=server.service, _=gettext, diff --git a/web/pgadmin/browser/server_groups/servers/roles/tests/utils.py b/web/pgadmin/browser/server_groups/servers/roles/tests/utils.py index 3a7ee58fd..028ee645d 100644 --- a/web/pgadmin/browser/server_groups/servers/roles/tests/utils.py +++ b/web/pgadmin/browser/server_groups/servers/roles/tests/utils.py @@ -152,3 +152,38 @@ def delete_role(connection, role_names): exception = "Error while deleting role: %s: line:%s %s" % ( file_name, sys.exc_traceback.tb_lineno, exception) print(exception, file=sys.stderr) + + +def create_role_with_password(server, role_name, role_password): + """ + This function create the role. + :param server: + :param role_name: + :param role_password: + :return: + """ + try: + connection = utils.get_db_connection(server['db'], + server['username'], + server['db_password'], + server['host'], + server['port'], + server['sslmode']) + pg_cursor = connection.cursor() + pg_cursor.execute( + "CREATE ROLE %s LOGIN PASSWORD '%s'" % (role_name, role_password)) + connection.commit() + # Get 'oid' from newly created tablespace + pg_cursor.execute( + "SELECT pr.oid from pg_catalog.pg_roles pr WHERE pr.rolname='%s'" % + role_name) + oid = pg_cursor.fetchone() + role_id = '' + if oid: + role_id = oid[0] + connection.close() + return role_id + except Exception as exception: + exception = "Error while deleting role: %s: line:%s %s" % ( + file_name, sys.exc_traceback.tb_lineno, exception) + print(exception, file=sys.stderr) diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 976e96202..c3fdaf374 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -95,6 +95,15 @@ class ServerGroup(db.Model): name = db.Column(db.String(128), nullable=False) __table_args__ = (db.UniqueConstraint('user_id', 'name'),) + @property + def serialize(self): + """Return object data in easily serializable format""" + return { + 'id': self.id, + 'user_id': self.user_id, + 'name': self.name, + } + class Server(db.Model): """Define a registered Postgres server""" @@ -176,6 +185,44 @@ class Server(db.Model): tunnel_password = db.Column(db.String(64), nullable=True) shared = db.Column(db.Boolean(), nullable=False) + @property + def serialize(self): + """Return object data in easily serializable format""" + return { + "id": self.id, + "user_id": self.user_id, + "servergroup_id": self.servergroup_id, + "name": self.name, + "host": self.host, + "hostaddr": self.hostaddr, + "port": self.port, + "maintenance_db": self.maintenance_db, + "username": self.username, + "password": self.password, + "save_password": self.save_password, + "role": self.role, + "ssl_mode": self.ssl_mode, + "comment": self.comment, + "discovery_id": self.discovery_id, + "db_res": self.db_res, + "passfile": self.passfile, + "sslcert": self.sslcert, + "sslkey": self.sslkey, + "sslrootcert": self.sslrootcert, + "sslcrl": self.sslcrl, + "sslcompression": self.sslcompression, + "bgcolor": self.bgcolor, + "fgcolor": self.fgcolor, + "service": self.service, + "connect_timeout": self.connect_timeout, + "use_ssh_tunnel": self.use_ssh_tunnel, + "tunnel_host": self.tunnel_host, + "tunnel_port": self.tunnel_port, + "tunnel_authentication": self.tunnel_authentication, + "tunnel_identity_file": self.tunnel_identity_file, + "tunnel_password": self.tunnel_password + } + class ModulePreference(db.Model): """Define a preferences table for any modules.""" diff --git a/web/pgadmin/static/js/sqleditor/new_connection_dialog.js b/web/pgadmin/static/js/sqleditor/new_connection_dialog.js new file mode 100644 index 000000000..3fcd37474 --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/new_connection_dialog.js @@ -0,0 +1,262 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2020, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import url_for from 'sources/url_for'; +import $ from 'jquery'; +import Alertify from 'pgadmin.alertifyjs'; +import pgAdmin from 'sources/pgadmin'; +import Backform from 'pgadmin.backform'; +import newConnectionDialogModel from 'sources/sqleditor/new_connection_dialog_model'; + + +let NewConnectionDialog = { + 'dialog': function(handler, reconnect) { + let url = url_for('sqleditor.get_new_connection_data', { + 'sid': handler.url_params.sid, + 'sgid': handler.url_params.sgid, + }); + + if(reconnect) { + url += '?connect=1'; + } + + let title = gettext('Connect to server'); + + $.ajax({ + url: url, + headers: { + 'Cache-Control' : 'no-cache', + }, + }).done(function (res) { + let response = res.data.result; + response.database_list = []; + response.user_list = []; + if (Alertify.newConnectionDialog) { + delete Alertify.newConnectionDialog; + } + + // Create Dialog + Alertify.dialog('newConnectionDialog', function factory() { + let $container = $('
'); + return { + main: function(message) { + this.msg = message; + }, + build: function() { + this.elements.content.appendChild($container.get(0)); + Alertify.pgDialogBuild.apply(this); + }, + setup: function(){ + return { + buttons: [ + { + text: '', + key: 112, + className: 'btn btn-primary-icon pull-left fa fa-question pg-alertify-icon-button', + attrs: { + name: 'dialog_help', + type: 'button', + label: gettext('Help'), + 'aria-label': gettext('Help'), + url: url_for('help.static', { + 'filename': 'query_tool.html', + }), + }, + }, + { + text: gettext('Cancel'), + key: 27, + className: 'btn btn-secondary fa fa-times pg-alertify-button', + 'data-btn-name': 'cancel', + }, { + text: gettext('OK'), + key: 13, + className: 'btn btn-primary fa fa-check pg-alertify-button', + 'data-btn-name': 'ok', + }, + ], + // Set options for dialog + options: { + title: title, + //disable both padding and overflow control. + padding: !1, + overflow: !1, + model: 0, + resizable: true, + maximizable: false, + pinnable: false, + closableByDimmer: false, + modal: false, + autoReset: false, + closable: true, + }, + }; + }, + prepare: function() { + let self = this; + $container.html(''); + // Disable Ok button + this.__internal.buttons[2].element.disabled = true; + + // Status bar + this.statusBar = $( + '
' + + ' ' + + '
').appendTo($container); + + // To show progress on filter Saving/Updating on AJAX + this.showNewConnectionProgress = $( + `
+
+
+
` + gettext('Loading data...') + `
+
+
` + ).appendTo($container); + $( + self.showNewConnectionProgress[0] + ).removeClass('d-none'); + + self.newConnCollectionModel = newConnectionDialogModel(response, handler.url_params.sgid, handler.url_params.sid); + let fields = Backform.generateViewSchema(null, self.newConnCollectionModel, 'create', null, null, true); + + let view = this.view = new Backform.Dialog({ + el: '
', + model: self.newConnCollectionModel, + schema: fields, + }); + + $(this.elements.body.childNodes[0]).addClass( + 'alertify_tools_dialog_properties obj_properties' + ); + + $container.append(view.render().$el); + + // Enable/disable save button and show/hide statusbar based on session + view.listenTo(view.model, 'pgadmin-session:start', function() { + view.listenTo(view.model, 'pgadmin-session:invalid', function(msg) { + self.statusBar.removeClass('d-none'); + $(self.statusBar.find('.alert-text')).html(msg); + // Disable Okay button + self.__internal.buttons[2].element.disabled = true; + }); + + view.listenTo(view.model, 'pgadmin-session:valid', function() { + self.statusBar.addClass('d-none'); + $(self.statusBar.find('.alert-text')).html(''); + // Enable Okay button + self.__internal.buttons[2].element.disabled = false; + }); + }); + + view.listenTo(view.model, 'pgadmin-session:stop', function() { + view.stopListening(view.model, 'pgadmin-session:invalid'); + view.stopListening(view.model, 'pgadmin-session:valid'); + }); + + // Starts monitoring changes to model + view.model.startNewSession(); + + // Hide Progress ... + $( + self.showNewConnectionProgress[0] + ).addClass('d-none'); + }, + callback: function(e) { + let self = this; + if (e.button.element.name == 'dialog_help') { + e.cancel = true; + pgAdmin.Browser.showHelp(e.button.element.name, e.button.element.getAttribute('url'), + null, null); + return; + } else if (e.button['data-btn-name'] === 'ok') { + e.cancel = true; // Do not close dialog + let newConnCollectionModel = this.newConnCollectionModel.toJSON(); + + let selected_database_name = null; + response.database_list.forEach(function(data){ + if(newConnCollectionModel['database'] == data['value']) { + selected_database_name = data['label']; + return false; + } + }); + let tab_title = ''; + if(newConnCollectionModel['role']) { + tab_title = selected_database_name + '/' + newConnCollectionModel['role'] + '@' + response.server_name; + } else { + tab_title = selected_database_name + '/' + newConnCollectionModel['user'] + '@' + response.server_name; + newConnCollectionModel['role'] = null; + } + + let is_create_connection = true; + + handler.gridView.connection_list.forEach(function(connection_data){ + if(parseInt(connection_data['server']) == newConnCollectionModel['server'] + && parseInt(connection_data['database']) == newConnCollectionModel['database'] + && connection_data['user'] == newConnCollectionModel['user'] && connection_data['role'] == newConnCollectionModel['role']) { + is_create_connection = false; + // break for loop by return false. + return false; + } + + if(tab_title == connection_data['title']) { + is_create_connection = false; + return false; + } + }); + if(!is_create_connection) { + let errmsg = 'Connection with this configuration already present.'; + Alertify.info(errmsg); + }else { + let connection_details = { + 'server_group': handler.gridView.handler.url_params.sgid, + 'server': newConnCollectionModel['server'], + 'database': newConnCollectionModel['database'], + 'title': tab_title, + 'user': newConnCollectionModel['user'], + 'role': newConnCollectionModel['role'], + 'password': response.password, + }; + handler.gridView.on_change_connection(connection_details, self); + } + } else { + self.close(); + } + }, + }; + }); + setTimeout(function(){ + Alertify.newConnectionDialog('Connect to server.').resizeTo(pgAdmin.Browser.stdW.md,pgAdmin.Browser.stdH.md); + }, 500); + }).fail(function(error) { + Alertify.alert().setting({ + 'title': gettext('Connection lost'), + 'label':gettext('Ok'), + 'message': gettext('Connection to the server has been lost.'), + 'onok': function(){ + alert(error); + //Close the window after connection is lost + window.close(); + }, + }).show(); + }); + + }, + +}; + +module.exports = NewConnectionDialog; diff --git a/web/pgadmin/static/js/sqleditor/new_connection_dialog_model.js b/web/pgadmin/static/js/sqleditor/new_connection_dialog_model.js new file mode 100644 index 000000000..e262d0e9d --- /dev/null +++ b/web/pgadmin/static/js/sqleditor/new_connection_dialog_model.js @@ -0,0 +1,339 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2020, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import gettext from 'sources/gettext'; +import _ from 'underscore'; +import $ from 'jquery'; +import pgAdmin from 'sources/pgadmin'; +import Backform from 'pgadmin.backform'; +import url_for from 'sources/url_for'; +import alertify from 'pgadmin.alertifyjs'; + +export default function newConnectionDialogModel(response, sgid, sid) { + + let server_name = ''; + let database_name = ''; + + let NewConnectionSelect2Control = Backform.Select2Control.extend({ + fetchData: function(){ + let self = this; + let url = self.field.get('url'); + + url = url_for(url, { + 'sid': self.model.attributes.server, + 'sgid': sgid, + }); + + $.ajax({ + async: false, + url: url, + headers: { + 'Cache-Control' : 'no-cache', + }, + }).done(function (res) { + var transform = self.field.get('transform'); + if(res.data.status){ + let data = res.data.result.data; + + if (transform && _.isFunction(transform)) { + self.field.set('options', transform.bind(self, data)); + } else { + self.field.set('options', data); + } + } else { + if (transform && _.isFunction(transform)) { + self.field.set('options', transform.bind(self, [])); + } else { + self.field.set('options', []); + } + //alertify.error(res.data.msg); + } + }).fail(function(e){ + let msg = ''; + if(e.status == 404) { + msg = 'Unable to find url.'; + } else { + msg = e.responseJSON.errormsg; + } + alertify.error(msg); + }); + }, + render: function() { + this.fetchData(); + return Backform.Select2Control.prototype.render.apply(this, arguments); + }, + onChange: function() { + Backform.Select2Control.prototype.onChange.apply(this, arguments); + }, + }); + + let newConnectionModel = pgAdmin.Browser.DataModel.extend({ + idAttribute: 'name', + defaults: { + server: parseInt(sid), + database: null, + user: null, + password: null, + server_name: server_name, + database_name: database_name, + }, + schema: [{ + id: 'server', + name: 'server', + label: gettext('Server'), + type: 'text', + editable: true, + disabled: false, + select2: { + allowClear: false, + }, + control: Backform.Select2Control.extend({ + connect: function(self) { + let local_self = self; + if(!alertify.connectServer){ + alertify.dialog('connectServer', function factory() { + return { + main: function( + title, message, server_id, submit_password=true + ) { + this.set('title', title); + this.message = message; + this.server_id = server_id; + this.submit_password = submit_password; + }, + setup:function() { + return { + buttons:[{ + text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button', + key: 27, + },{ + text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button', + }], + focus: {element: '#password', select: true}, + options: { + modal: 0, resizable: false, maximizable: false, pinnable: false, + }, + }; + }, + build:function() { + }, + prepare:function() { + this.setContent(this.message); + }, + callback: function(closeEvent) { + + if (closeEvent.button.text == gettext('OK')) { + if(this.submit_password) { + var _url = url_for('sqleditor.connect_server', {'sid': this.server_id}); + + $.ajax({ + type: 'POST', + timeout: 30000, + url: _url, + data: $('#frmPassword').serialize(), + }) + .done(function() { + local_self.model.attributes.database = null; + local_self.model.attributes.user = null; + local_self.model.attributes.role = null; + Backform.Select2Control.prototype.onChange.apply(local_self, arguments); + response.server_list.forEach(function(obj){ + if(obj.id==self.model.changed.server) { + response.server_name = obj.name; + } + }); + }) + .fail(function(xhr) { + alertify.connectServer('Connect to server', xhr.responseJSON.result, local_self.getValueFromDOM()); + }); + } else { + response.password = $('#password').val(); + } + } else { + local_self.model.attributes.database = null; + local_self.model.attributes.user = null; + local_self.model.attributes.role = null; + Backform.Select2Control.prototype.onChange.apply(local_self, arguments); + } + closeEvent.close = true; + }, + }; + }); + } + }, + render: function() { + let self = this; + self.connect(self); + return Backform.Select2Control.prototype.render.apply(self, arguments); + }, + onChange: function() { + this.model.attributes.database = null; + this.model.attributes.user = null; + let self = this; + self.connect(self); + + let url = url_for('sqleditor.connect_server', { + 'sid': self.getValueFromDOM(), + 'usr': self.model.attributes.user, + }); + $.ajax({ + async: false, + url: url, + type: 'POST', + headers: { + 'Cache-Control' : 'no-cache', + }, + }).done(function () { + Backform.Select2Control.prototype.onChange.apply(self, arguments); + response.server_list.forEach(function(obj){ + if(obj.id==self.model.changed.server) { + response.server_name = obj.name; + } + }); + }).fail(function(xhr){ + alertify.connectServer('Connect to server', xhr.responseJSON.result, self.getValueFromDOM()); + }); + + }, + }), + options: function() { + return _.map(response.server_list, (obj) => { + if (obj.id == parseInt(sid)) + response.server_name = obj.name; + + return { + value: obj.id, + label: obj.name, + }; + }); + }, + }, + { + id: 'database', + name: 'database', + label: gettext('Database'), + type: 'text', + editable: true, + disabled: function(m) { + let self_local = this; + if (!_.isUndefined(m.get('server')) && !_.isNull(m.get('server')) + && m.get('server') !== '') { + setTimeout(function() { + if(self_local.options.length) { + m.set('database', self_local.options[0].value); + } + }, 10); + return false; + } + + return true; + }, + deps: ['server'], + url: 'sqleditor.get_new_connection_database', + select2: { + allowClear: false, + width: '100%', + first_empty: true, + select_first: false, + }, + extraClasses:['new-connection-dialog-style'], + control: NewConnectionSelect2Control, + transform: function(data) { + response.database_list = data; + return data; + }, + }, + { + id: 'user', + name: 'user', + label: gettext('User'), + type: 'text', + editable: true, + deps: ['server'], + select2: { + allowClear: false, + width: '100%', + }, + control: NewConnectionSelect2Control, + url: 'sqleditor.get_new_connection_user', + disabled: function(m) { + let self_local = this; + if (!_.isUndefined(m.get('server')) && !_.isNull(m.get('server')) + && m.get('server') !== '') { + setTimeout(function() { + if(self_local.options.length) { + m.set('user', self_local.options[0].value); + } + }, 10); + return false; + } + return true; + }, + },{ + id: 'role', + name: 'role', + label: gettext('Role'), + type: 'text', + editable: true, + deps: ['server'], + select2: { + allowClear: false, + width: '100%', + first_empty: true, + }, + control: NewConnectionSelect2Control, + url: 'sqleditor.get_new_connection_role', + disabled: false, + }, + /*{ + id: 'password', + name: 'password', + label: gettext('Password'tools/sqleditor/__init__.py), + type: 'password', + editable: true, + disabled: true, + deps: ['user'], + control: Backform.InputControl.extend({ + render: function() { + let self = this; + self.model.attributes.password = null; + Backform.InputControl.prototype.render.apply(self, arguments); + return self; + }, + onChange: function() { + let self = this; + Backform.InputControl.prototype.onChange.apply(self, arguments); + }, + }), + },*/ + ], + validate: function() { + let msg = null; + this.errorModel.clear(); + if(_.isUndefined(this.get('database')) || _.isNull(this.get('database'))){ + msg = gettext('Please select database'); + this.errorModel.set('database', msg); + return msg; + } else if(_.isUndefined(this.get('database')) || _.isUndefined(this.get('user'))|| _.isNull(this.get('user'))) { + msg = gettext('Please select user'); + this.errorModel.set('user', msg); + return msg; + } + /*else if((this.attributes.password == '' || _.isUndefined(this.get('password')) || _.isNull(this.get('password')))) { + msg = gettext('Please enter password'); + this.errorModel.set('password', msg); + return msg; + }*/ + return null; + }, + }); + + let model = new newConnectionModel(); + return model; +} diff --git a/web/pgadmin/static/scss/_alert.scss b/web/pgadmin/static/scss/_alert.scss index dac552bed..836f0af93 100644 --- a/web/pgadmin/static/scss/_alert.scss +++ b/web/pgadmin/static/scss/_alert.scss @@ -92,6 +92,7 @@ right: 0; left: 0; bottom: 0; + z-index: 1; } .pg-prop-status-bar { diff --git a/web/pgadmin/tools/datagrid/__init__.py b/web/pgadmin/tools/datagrid/__init__.py index 1bd841f20..f7b836a9f 100644 --- a/web/pgadmin/tools/datagrid/__init__.py +++ b/web/pgadmin/tools/datagrid/__init__.py @@ -18,22 +18,23 @@ from flask import Response, url_for, session, request, make_response from werkzeug.useragents import UserAgent from flask import current_app as app, render_template from flask_babelex import gettext -from flask_security import login_required +from flask_security import login_required, current_user from pgadmin.tools.sqleditor.command import ObjectRegistry, SQLFilter +from pgadmin.tools.sqleditor import check_transaction_status from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response, bad_request, \ - internal_server_error + internal_server_error, unauthorized from config import PG_DEFAULT_DRIVER -from pgadmin.model import Server +from pgadmin.model import Server, User from pgadmin.utils.driver import get_driver from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost from pgadmin.utils.preferences import Preferences from pgadmin.settings import get_setting from pgadmin.browser.utils import underscore_unescape from pgadmin.utils.exception import ObjectGone -from pgadmin.utils.constants import MIMETYPE_APP_JS from pgadmin.tools.sqleditor.utils.macros import get_user_macros +from pgadmin.utils.constants import MIMETYPE_APP_JS, UNAUTH_REQ MODULE_NAME = 'datagrid' @@ -74,7 +75,8 @@ class DataGridModule(PgAdminModule): 'datagrid.filter_validate', 'datagrid.filter', 'datagrid.panel', - 'datagrid.close' + 'datagrid.close', + 'datagrid.update_query_tool_connection' ] def on_logout(self, user): @@ -324,10 +326,48 @@ def initialize_query_tool(trans_id, sgid, sid, did=None): req_args['recreate'] == '1'): connect = False + is_error, errmsg, conn_id, version = _init_query_tool(trans_id, connect, + sgid, sid, did) + if is_error: + return errmsg + + return make_json_response( + data={ + 'connId': str(conn_id), + 'serverVersion': version, + } + ) + + +def _connect(conn, **kwargs): + """ + Connect the database. + :param conn: Connection instance. + :param kwargs: user, role and password data from user. + :return: + """ + user = None + role = None + password = None + is_ask_password = False + if 'user' in kwargs and 'role' in kwargs: + user = kwargs['user'] + role = kwargs['role'] if kwargs['role'] else None + password = kwargs['password'] if kwargs['password'] else None + is_ask_password = True + if user: + status, msg = conn.connect(user=user, role=role, + password=password) + else: + status, msg = conn.connect() + + return status, msg, is_ask_password, user, role, password + + +def _init_query_tool(trans_id, connect, sgid, sid, did, **kwargs): # Create asynchronous connection using random connection id. conn_id = str(random.randint(1, 9999999)) - # Use Maintenance database OID manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) if did is None: @@ -338,24 +378,41 @@ def initialize_query_tool(trans_id, sgid, sid, did=None): ) except Exception as e: app.logger.error(e) - return internal_server_error(errormsg=str(e)) + return True, internal_server_error(errormsg=str(e)), '', '' try: conn = manager.connection(did=did, conn_id=conn_id, auto_reconnect=False, use_binary_placeholder=True, array_to_string=True) + if connect: - status, msg = conn.connect() + status, msg, is_ask_password, user, role, password = _connect( + conn, **kwargs) if not status: app.logger.error(msg) - return internal_server_error(errormsg=str(msg)) + if is_ask_password: + server = Server.query.filter_by(id=sid).first() + return True, make_json_response( + success=0, + status=428, + result=render_template( + 'servers/password.html', + server_label=server.name, + username=user, + errmsg=msg, + _=gettext, + ) + ), '', '' + else: + return True, internal_server_error( + errormsg=str(msg)), '', '' except (ConnectionLost, SSHTunnelConnectionLost) as e: app.logger.error(e) raise except Exception as e: app.logger.error(e) - return internal_server_error(errormsg=str(e)) + return True, internal_server_error(errormsg=str(e)), '', '' if 'gridData' not in session: sql_grid_data = dict() @@ -377,10 +434,77 @@ def initialize_query_tool(trans_id, sgid, sid, did=None): # Store the grid dictionary into the session variable session['gridData'] = sql_grid_data + return False, '', conn_id, manager.version + + +@blueprint.route( + '/initialize/query_tool/update_connection//' + '//', + methods=["POST"], endpoint='update_query_tool_connection' +) +def update_query_tool_connection(trans_id, sgid, sid, did): + # Remove transaction Id. + with query_tool_close_session_lock: + data = json.loads(request.data, encoding='utf-8') + + if 'gridData' not in session: + return make_json_response(data={'status': True}) + + grid_data = session['gridData'] + + # Return from the function if transaction id not found + if str(trans_id) not in grid_data: + return make_json_response(data={'status': True}) + + connect = True + + req_args = request.args + if ('recreate' in req_args and + req_args['recreate'] == '1'): + connect = False + + new_trans_id = str(random.randint(1, 9999999)) + kwargs = { + 'user': data['user'], + 'role': data['role'], + 'password': data['password'] if 'password' in data else None + } + + is_error, errmsg, conn_id, version = _init_query_tool( + new_trans_id, connect, sgid, sid, did, **kwargs) + + if is_error: + return errmsg + else: + try: + # Check the transaction and connection status + status, error_msg, conn, trans_obj, session_obj = \ + check_transaction_status(trans_id) + + status, error_msg, new_conn, new_trans_obj, new_session_obj = \ + check_transaction_status(new_trans_id) + + new_session_obj['primary_keys'] = session_obj[ + 'primary_keys'] if 'primary_keys' in session_obj else None + new_session_obj['columns_info'] = session_obj[ + 'columns_info'] if 'columns_info' in session_obj else None + new_session_obj['client_primary_key'] = session_obj[ + 'client_primary_key'] if 'client_primary_key'\ + in session_obj else None + + close_query_tool_session(trans_id) + # Remove the information of unique transaction id from the + # session variable. + grid_data.pop(str(trans_id), None) + session['gridData'] = grid_data + except Exception as e: + app.logger.error(e) + return make_json_response( data={ 'connId': str(conn_id), - 'serverVersion': manager.version, + 'serverVersion': version, + 'tran_id': new_trans_id } ) diff --git a/web/pgadmin/tools/datagrid/templates/datagrid/index.html b/web/pgadmin/tools/datagrid/templates/datagrid/index.html index a0eebc8cf..4970027e9 100644 --- a/web/pgadmin/tools/datagrid/templates/datagrid/index.html +++ b/web/pgadmin/tools/datagrid/templates/datagrid/index.html @@ -417,8 +417,17 @@ title="" role="img"> -
 
+
+ + + +
+ +
@@ -481,6 +490,7 @@ require(['sources/generated/browser_nodes', 'sources/generated/codemirror', 'sou var script_type_url = ''; {% endif %} // Start the query tool. + sqlEditorController.start( {{ uniqueId }}, {{ url_params|safe}}, diff --git a/web/pgadmin/tools/datagrid/tests/__init__.py b/web/pgadmin/tools/datagrid/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/tools/datagrid/tests/datagrid_test_data.json b/web/pgadmin/tools/datagrid/tests/datagrid_test_data.json new file mode 100644 index 000000000..0075f35e3 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/datagrid_test_data.json @@ -0,0 +1,134 @@ +{ + "data_grid_init_query_tool": [ + { + "name": "Datagrid init query tool", + "url": "/datagrid/initialize/query_tool/", + "is_positive_test": true, + "mocking_required": false, + "test_data": {}, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "data_grid_query_tool_close": [ + { + "name": "Datagrid query tool close", + "url": "/datagrid/close/", + "is_positive_test": true, + "mocking_required": false, + "test_data": {}, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "data_grid_validate_filter": [ + { + "name": "Datagrid validate filter", + "url": "/datagrid/filter/validate/", + "is_positive_test": true, + "mocking_required": false, + "test_data": "id = 1", + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Datagrid validate filter", + "url": "/datagrid/filter/validate/", + "is_positive_test": false, + "mocking_required": true, + "test_data": "id = 1", + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar", + "return_value": "(False, 'Mocked Internal Server Error while validate filter')" + }, + "expected_data": { + "status_code": 200 + } + } + ], + "data_grid_update_connection": [ + { + "name": "Datagrid update connection positive", + "url": "/datagrid/initialize/query_tool/update_connection/", + "is_positive_test": true, + "mocking_required": false, + "is_create_role": false, + "test_data": {}, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Datagrid update connection with new user", + "url": "/datagrid/initialize/query_tool/update_connection/", + "is_positive_test": true, + "mocking_required": false, + "is_create_role": true, + "test_data": {}, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "data_grid_panel": [ + { + "name": "Datagrid Panel", + "url": "/datagrid/panel/", + "is_positive_test": true, + "mocking_required": false, + "test_data": {}, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + } + ], + "data_grid_initialize": [ + { + "name": "Datagrid Initialize", + "url": "/datagrid/initialize/datagrid/", + "is_positive_test": true, + "mocking_required": false, + "test_data": "id=1", + "mock_data": { + + }, + "expected_data": { + "status_code": 200 + } + },{ + "name": "Datagrid Initialize", + "url": "/datagrid/initialize/datagrid/", + "is_positive_test": true, + "mocking_required": false, + "test_data": null, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Datagrid Initialize", + "url": "/datagrid/initialize/datagrid/", + "is_positive_test": false, + "mocking_required": true, + "test_data": "id=1", + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(False, 'Mocked Internal Server Error while initialize datagrid.')" + }, + "expected_data": { + "status_code": 500 + } + } + ] +} diff --git a/web/pgadmin/tools/datagrid/tests/test_data_grid_init_query_tool.py b/web/pgadmin/tools/datagrid/tests/test_data_grid_init_query_tool.py new file mode 100644 index 000000000..f64e561d5 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_data_grid_init_query_tool.py @@ -0,0 +1,73 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.utils.exception import ExecuteError +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from . import utils as data_grid_utils + + +class DatagridInitQueryToolTestCase(BaseTestGenerator): + """ + This will init query-tool connection. + """ + + scenarios = utils.generate_scenarios( + 'data_grid_init_query_tool', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + + self.trans_id = str(random.randint(1, 9999999)) + + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + + def init_query_tool(self): + response = self.tester.post( + self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' + str( + self.sid) + '/' + str(self.did), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will init query tool connection.""" + + if self.is_positive_test: + response = self.init_query_tool() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/test_data_grid_panel.py b/web/pgadmin/tools/datagrid/tests/test_data_grid_panel.py new file mode 100644 index 000000000..54bbe314d --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_data_grid_panel.py @@ -0,0 +1,90 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from pgadmin.utils.exception import ExecuteError +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from . import utils as data_grid_utils + + +class DatagridPanelTestCase(BaseTestGenerator): + """ + This will data grid panel. + """ + + scenarios = utils.generate_scenarios( + 'data_grid_panel', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + + self.trans_id = str(random.randint(1, 9999999)) + qt_init = data_grid_utils._init_query_tool(self, self.trans_id, + self.sgid, self.sid, + self.did) + + if not qt_init['success']: + raise ExecuteError("Could not initialize querty tool.") + + def panel(self): + query_param = \ + '?is_query_tool={0}&sgid={1}&sid={2}&server_type={3}' \ + '&did={4}&title={5}'.format(True, self.sgid, self.sid, + self.server_information['type'], + self.did, 'Query panel') + + response = self.tester.post( + self.url + str(self.trans_id) + query_param, + data=json.dumps(self.test_data), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will update query tool connection.""" + + if self.is_positive_test: + response = self.panel() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.panel() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/test_data_grid_query_tool_close.py b/web/pgadmin/tools/datagrid/tests/test_data_grid_query_tool_close.py new file mode 100644 index 000000000..7e4b56c95 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_data_grid_query_tool_close.py @@ -0,0 +1,78 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from . import utils as data_grid_utils +from pgadmin.utils.exception import ExecuteError + + +class DatagridQueryToolCloseTestCase(BaseTestGenerator): + """ + This will close query-tool connection. + """ + + scenarios = utils.generate_scenarios( + 'data_grid_query_tool_close', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + + self.trans_id = str(random.randint(1, 9999999)) + qt_init = data_grid_utils._init_query_tool(self, self.trans_id, + self.sgid, self.sid, + self.did) + + if not qt_init['success']: + raise ExecuteError("Could not initialize querty tool.") + + def close_connection(self): + response = self.tester.delete( + self.url + str(self.trans_id), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will update query tool connection.""" + + if self.is_positive_test: + response = self.close_connection() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/test_data_grid_update_connection.py b/web/pgadmin/tools/datagrid/tests/test_data_grid_update_connection.py new file mode 100644 index 000000000..a15702cc1 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_data_grid_update_connection.py @@ -0,0 +1,121 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from pgadmin.browser.server_groups.servers.roles.tests import \ + utils as roles_utils +from . import utils as data_grid_utils +from pgadmin.utils.exception import ExecuteError + + +class DatagridUpdateConnectionTestCase(BaseTestGenerator): + """ + This will update query-tool connection. + """ + + scenarios = utils.generate_scenarios( + 'data_grid_update_connection', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + + self.trans_id = str(random.randint(1, 9999999)) + self.roles = None + + if self.is_create_role: + data = roles_utils.get_role_data(self.server['db_password']) + self.role_name = data['rolname'] + self.role_password = data['rolpassword'] + roles_utils.create_role_with_password( + self.server, self.role_name, self.role_password) + + if not self.is_positive_test or self.is_create_role: + qt_init = data_grid_utils._init_query_tool(self, self.trans_id, + self.sgid, self.sid, + self.did) + + if not qt_init['success']: + raise ExecuteError("Could not initialize querty tool.") + + self.test_data = { + "database": self.did, + "server": self.sid, + } + + if self.server_information['type'] == 'ppas': + self.test_data['password'] = 'enterprisedb' + self.test_data['user'] = 'enterprisedb' + else: + self.test_data['password'] = 'postgres' + self.test_data['user'] = 'postgres' + + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + + def update_connection(self, user_data=None): + if user_data: + response = self.tester.post( + self.url + str(self.trans_id) + '/' + str(self.sgid) + + '/' + str(self.sid) + '/' + str(self.did), + data=json.dumps(user_data), + content_type='html/json' + ) + else: + response = self.tester.post( + self.url + str(self.trans_id) + '/' + str(self.sgid) + '/' + + str(self.sid) + '/' + str(self.did), + data=json.dumps(self.test_data), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will update query tool connection.""" + + if self.is_positive_test: + user_data = dict() + if self.is_create_role: + user_data['user'] = self.role_name + user_data['password'] = self.role_password + user_data['role'] = None + response = self.update_connection(user_data=user_data) + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + response = self.update_connection() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/test_data_grid_validate_filter.py b/web/pgadmin/tools/datagrid/tests/test_data_grid_validate_filter.py new file mode 100644 index 000000000..b467b6dc2 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_data_grid_validate_filter.py @@ -0,0 +1,92 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from . import utils as data_grid_utils +from pgadmin.utils.exception import ExecuteError + + +class DatagridValidateFilterTestCase(BaseTestGenerator): + """ + This will validate filter connection. + """ + + scenarios = utils.generate_scenarios( + 'data_grid_validate_filter', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + self.schema_id = parent_node_dict['schema'][-1]["schema_id"] + self.schema_name = parent_node_dict['schema'][-1]["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise ExecuteError("Could not find the schema to add a table.") + self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + + def validate_filter(self): + response = self.tester.post( + self.url + str(self.sid) + '/' + str(self.did) + '/' + + str(self.table_id), + data=json.dumps(self.test_data), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will update query tool connection.""" + + if self.is_positive_test: + response = self.validate_filter() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.validate_filter() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/test_initialize_data_grid.py b/web/pgadmin/tools/datagrid/tests/test_initialize_data_grid.py new file mode 100644 index 000000000..8d07139dd --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/test_initialize_data_grid.py @@ -0,0 +1,109 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + + +import json +import uuid +import random + +from unittest.mock import patch +from pgadmin.browser.server_groups.servers.databases.tests import utils as \ + database_utils + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from regression.test_setup import config_data +from pgadmin.browser.server_groups.servers.databases.schemas.tests import \ + utils as schema_utils +from pgadmin.browser.server_groups.servers.databases.schemas.tables.tests \ + import utils as tables_utils +from . import utils as data_grid_utils +from pgadmin.utils.exception import ExecuteError + + +class DatagridInitializeTestCase(BaseTestGenerator): + """ + This will Initialize datagrid + """ + + scenarios = utils.generate_scenarios( + 'data_grid_initialize', + data_grid_utils.test_cases + ) + + def setUp(self): + self.database_info = parent_node_dict["database"][-1] + self.db_name = self.database_info["db_name"] + self.did = self.database_info["db_id"] + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + db_con = database_utils.connect_database(self, utils.SERVER_GROUP, + self.sid, self.did) + if not db_con['data']["connected"]: + raise ExecuteError("Could not connect to database to add a table.") + + self.schema_id = parent_node_dict['schema'][-1]["schema_id"] + self.schema_name = parent_node_dict['schema'][-1]["schema_name"] + schema_response = schema_utils.verify_schemas(self.server, + self.db_name, + self.schema_name) + if not schema_response: + raise ExecuteError("Could not find the schema to add a table.") + self.table_name = "table_for_wizard%s" % (str(uuid.uuid4())[1:8]) + self.table_id = tables_utils.create_table(self.server, self.db_name, + self.schema_name, + self.table_name) + self.trans_id = str(random.randint(1, 9999999)) + qt_init = data_grid_utils._init_query_tool(self, self.trans_id, + self.sgid, self.sid, + self.did) + + if not qt_init['success']: + raise ExecuteError("Could not initialize query tool.") + + def initialize_datagrid(self): + if self.test_data: + response = self.tester.post( + self.url + str(self.trans_id) + '/4/table/' + + str(self.sgid) + '/' + str(self.sid) + '/' + + str(self.did) + '/' + str(self.table_id), + data=json.dumps(self.test_data), + content_type='html/json' + ) + else: + response = self.tester.post( + self.url + str(self.trans_id) + '/4/table/' + + str(self.sgid) + '/' + str(self.sid) + '/' + + str(self.did) + '/' + str(self.table_id), + content_type='html/json' + ) + return response + + def runTest(self): + """ This function will update query tool connection.""" + + if self.is_positive_test: + response = self.initialize_datagrid() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + with patch(self.mock_data["function_name"], + return_value=eval(self.mock_data["return_value"])): + response = self.initialize_datagrid() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + + self.assertEqual(actual_response_code, expected_response_code) + + def tearDown(self): + """This function disconnect database.""" + database_utils.disconnect_database(self, self.sid, + self.did) diff --git a/web/pgadmin/tools/datagrid/tests/utils.py b/web/pgadmin/tools/datagrid/tests/utils.py new file mode 100644 index 000000000..c3d4bb555 --- /dev/null +++ b/web/pgadmin/tools/datagrid/tests/utils.py @@ -0,0 +1,33 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import os +import json + +file_name = os.path.basename(__file__) +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/datagrid_test_data.json") as data_file: + test_cases = json.load(data_file) + + +def _init_query_tool(self, trans_id, server_group, server_id, db_id): + QUERY_TOOL_INIT_URL = '/datagrid/initialize/query_tool' + + qt_init = self.tester.post( + '{0}/{1}/{2}/{3}/{4}'.format( + QUERY_TOOL_INIT_URL, + trans_id, + server_group, + server_id, + db_id + ), + follow_redirects=True + ) + assert qt_init.status_code == 200 + qt_init = json.loads(qt_init.data.decode('utf-8')) + return qt_init diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 0f56b603d..9da88421b 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -10,17 +10,15 @@ """A blueprint module implementing the sqleditor frame.""" import os import pickle -import sys import re - -import simplejson as json -from flask import Response, url_for, render_template, session, request, \ - current_app -from flask_babelex import gettext -from flask_security import login_required, current_user from urllib.parse import unquote +import simplejson as json from config import PG_DEFAULT_DRIVER, ON_DEMAND_RECORD_COUNT +from flask import Response, url_for, render_template, session, current_app +from flask import request, jsonify +from flask_babelex import gettext +from flask_security import login_required, current_user from pgadmin.misc.file_manager import Filemanager from pgadmin.tools.sqleditor.command import QueryToolCommand from pgadmin.tools.sqleditor.utils.constant_definition import ASYNC_OK, \ @@ -32,11 +30,11 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \ from pgadmin.utils import PgAdminModule from pgadmin.utils import get_storage_directory from pgadmin.utils.ajax import make_json_response, bad_request, \ - success_return, internal_server_error, make_response as ajax_response + success_return, internal_server_error from pgadmin.utils.driver import get_driver -from pgadmin.utils.menu import MenuItem -from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\ +from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost, \ CryptKeyMissing +from pgadmin.utils.menu import MenuItem from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete from pgadmin.tools.sqleditor.utils.query_tool_preferences import \ register_query_tool_preferences @@ -44,13 +42,16 @@ from pgadmin.tools.sqleditor.utils.query_tool_fs_utils import \ read_file_generator from pgadmin.tools.sqleditor.utils.filter_dialog import FilterDialog from pgadmin.tools.sqleditor.utils.query_history import QueryHistory -from pgadmin.utils.constants import MIMETYPE_APP_JS, SERVER_CONNECTION_CLOSED,\ - ERROR_MSG_TRANS_ID_NOT_FOUND from pgadmin.tools.sqleditor.utils.macros import get_macros,\ get_user_macros, set_macros +from pgadmin.utils.constants import MIMETYPE_APP_JS, \ + SERVER_CONNECTION_CLOSED, ERROR_MSG_TRANS_ID_NOT_FOUND, ERROR_FETCHING_DATA +from pgadmin.model import Server +from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry MODULE_NAME = 'sqleditor' TRANSACTION_STATUS_CHECK_FAILED = gettext("Transaction status check failed.") +_NODES_SQL = 'nodes.sql' class SqlEditorModule(PgAdminModule): @@ -114,7 +115,13 @@ class SqlEditorModule(PgAdminModule): 'sqleditor.clear_query_history', 'sqleditor.get_macro', 'sqleditor.get_macros', - 'sqleditor.set_macros' + 'sqleditor.set_macros', + 'sqleditor.get_new_connection_data', + 'sqleditor.get_new_connection_database', + 'sqleditor.get_new_connection_user', + 'sqleditor.get_new_connection_role', + 'sqleditor.connect_server', + 'sqleditor.connect_server_with_user', ] def register_preferences(self): @@ -230,7 +237,7 @@ def start_view_data(trans_id): ) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: # set fetched row count to 0 as we are executing query again. trans_obj.update_fetched_row_cnt(0) @@ -376,7 +383,7 @@ def poll(trans_id): if isinstance(trans_obj, QueryToolCommand): trans_status = conn.transaction_status() if trans_status == TX_STATUS_INERROR and \ - trans_obj.auto_rollback: + trans_obj.auto_rollback: conn.execute_void("ROLLBACK;") st, result = conn.async_fetchmany_2darray(ON_DEMAND_RECORD_COUNT) @@ -686,13 +693,12 @@ def save(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: # If there is no primary key found then return from the function. if ('primary_keys' not in session_obj or - len(session_obj['primary_keys']) <= 0 or - len(changed_data) <= 0) and \ - 'has_oids' not in session_obj: + len(session_obj['primary_keys']) <= 0 or + len(changed_data) <= 0) and 'has_oids' not in session_obj: return make_json_response( data={ 'status': False, @@ -759,7 +765,7 @@ def append_filter_inclusive(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None filter_sql = '' @@ -813,7 +819,7 @@ def append_filter_exclusive(trans_id): info='DATAGRID_TRANSACTION_REQUIRED', status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None filter_sql = '' @@ -866,7 +872,7 @@ def remove_filter(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None @@ -910,7 +916,7 @@ def set_limit(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None @@ -1052,7 +1058,7 @@ def get_object_name(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = trans_obj.object_name else: status = False @@ -1088,7 +1094,7 @@ def set_auto_commit(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None @@ -1133,7 +1139,7 @@ def set_auto_rollback(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: res = None @@ -1185,7 +1191,7 @@ def auto_complete(trans_id): status=404) if status and conn is not None and \ - trans_obj is not None and session_obj is not None: + trans_obj is not None and session_obj is not None: # Create object of SQLAutoComplete class and pass connection object auto_complete_obj = SQLAutoComplete( @@ -1472,6 +1478,282 @@ def get_filter_data(trans_id): return FilterDialog.get(status, error_msg, conn, trans_obj, session_ob) +@blueprint.route( + '/new_connection_dialog//', + methods=["GET"], endpoint='get_new_connection_data' +) +@login_required +def get_new_connection_data(sgid, sid=None): + """ + This method is used to get required data for get new connection. + :extract_sql_from_network_parameters, + """ + try: + # if sid and not did: + servers = Server.query.all() + server_list = [ + {'name': server.serialize['name'], "id": server.serialize['id']} + for server in servers] + + msg = "Success" + return make_json_response( + data={ + 'status': True, + 'msg': msg, + 'result': { + 'server_list': server_list + } + } + ) + + except Exception: + return make_json_response( + data={ + 'status': False, + 'msg': ERROR_FETCHING_DATA, + 'result': { + 'server_list': [] + } + } + ) + + +@blueprint.route( + '/new_connection_database//', + methods=["GET"], endpoint='get_new_connection_database' +) +@login_required +def get_new_connection_database(sgid, sid=None): + """ + This method is used to get required data for get new connection. + :extract_sql_from_network_parameters, + """ + try: + database_list = [] + from pgadmin.utils.driver import get_driver + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection() + if conn.connected(): + is_connected = True + else: + is_connected = False + if is_connected: + if sid: + template_path = 'databases/sql/#{0}#'.format(manager.version) + last_system_oid = 0 + server_node_res = manager + + db_disp_res = None + params = None + if server_node_res and server_node_res.db_res: + db_disp_res = ", ".join( + ['%s'] * len(server_node_res.db_res.split(',')) + ) + params = tuple(server_node_res.db_res.split(',')) + sql = render_template( + "/".join([template_path, _NODES_SQL]), + last_system_oid=last_system_oid, + db_restrictions=db_disp_res + ) + status, databases = conn.execute_dict(sql, params) + database_list = [ + {'label': database['name'], 'value': database['did']} for + database in databases['rows']] + else: + status = False + + msg = "Success" + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data': database_list, + } + } + ) + else: + return make_json_response( + data={ + 'status': False, + 'msg': SERVER_CONNECTION_CLOSED, + 'result': { + 'database_list': [], + } + } + ) + except Exception: + return make_json_response( + data={ + 'status': False, + 'msg': ERROR_FETCHING_DATA, + 'result': { + 'database_list': [], + } + } + ) + + +@blueprint.route( + '/new_connection_user//', + methods=["GET"], endpoint='get_new_connection_user' +) +@login_required +def get_new_connection_user(sgid, sid=None): + """ + This method is used to get required data for get new connection. + :extract_sql_from_network_parameters, + """ + try: + from pgadmin.utils.driver import get_driver + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection() + user_list = [] + if conn.connected(): + is_connected = True + else: + is_connected = False + if is_connected: + if sid: + sql_path = 'roles/sql/#{0}#'.format(manager.version) + status, users = conn.execute_2darray( + render_template(sql_path + _NODES_SQL) + ) + user_list = [ + {'value': user['rolname'], 'label': user['rolname']} for + user in users['rows'] if user['rolcanlogin']] + else: + status = False + + msg = "Success" + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data': user_list, + } + } + ) + else: + return make_json_response( + data={ + 'status': False, + 'msg': SERVER_CONNECTION_CLOSED, + 'result': { + 'user_list': [], + } + } + ) + except Exception: + return make_json_response( + data={ + 'status': False, + 'msg': 'Unable to fetch data.', + 'result': { + 'user_list': [], + } + } + ) + + +@blueprint.route( + '/new_connection_role//', + methods=["GET"], endpoint='get_new_connection_role' +) +@login_required +def get_new_connection_role(sgid, sid=None): + """ + This method is used to get required data for get new connection. + :extract_sql_from_network_parameters, + """ + try: + from pgadmin.utils.driver import get_driver + manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) + conn = manager.connection() + role_list = [] + if conn.connected(): + is_connected = True + else: + is_connected = False + if is_connected: + if sid: + sql_path = 'roles/sql/#{0}#'.format(manager.version) + status, roles = conn.execute_2darray( + render_template(sql_path + _NODES_SQL) + ) + role_list = [ + {'value': role['rolname'], 'label': role['rolname']} for + role in roles['rows']] + else: + status = False + + msg = "Success" + return make_json_response( + data={ + 'status': status, + 'msg': msg, + 'result': { + 'data': role_list, + } + } + ) + else: + return make_json_response( + data={ + 'status': False, + 'msg': SERVER_CONNECTION_CLOSED, + 'result': { + 'user_list': [], + } + } + ) + except Exception: + return make_json_response( + data={ + 'status': False, + 'msg': 'Unable to fetch data.', + 'result': { + 'user_list': [], + } + } + ) + + +@blueprint.route( + '/connect_server//', + methods=["POST"], + endpoint="connect_server_with_user" +) +@blueprint.route( + '/connect_server/', + methods=["POST"], + endpoint="connect_server" +) +@login_required +def connect_server(sid, usr=None): + # Check if server is already connected then no need to reconnect again. + server = Server.query.filter_by(id=sid).first() + driver = get_driver(PG_DEFAULT_DRIVER) + manager = driver.connection_manager(sid) + conn = manager.connection() + user = None + + if usr and manager.user != usr: + user = usr + else: + user = manager.user + if conn.connected(): + return make_json_response( + success=1, + info=gettext("Server connected."), + data={} + ) + + view = SchemaDiffRegistry.get_node_view('server') + return view.connect(server.servergroup_id, sid, user_name=user) + + @blueprint.route( '/filter_dialog/', methods=["PUT"], endpoint='set_filter_data' diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css index c281d537e..20590d362 100644 --- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css +++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css @@ -315,10 +315,6 @@ input.editor-checkbox:focus { padding: 10px 0px; } -.editor-title { - width:100%; -} - .connection-status-hide { display: none !important; } @@ -396,7 +392,6 @@ input.editor-checkbox:focus { overflow-y: hidden; } - /* Macros */ .macro-tab { @@ -424,3 +419,7 @@ input.editor-checkbox:focus { .macro_dialog .pg-prop-status-bar { z-index: 1; } + +.new-connection-dialog-style { + width: 100% !important; +} diff --git a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js index ed3bd59ea..4ef4b8fd2 100644 --- a/web/pgadmin/tools/sqleditor/static/js/sqleditor.js +++ b/web/pgadmin/tools/sqleditor/static/js/sqleditor.js @@ -14,6 +14,7 @@ define('tools.querytool', [ 'jqueryui.position', 'underscore', 'pgadmin.alertifyjs', 'sources/pgadmin', 'backbone', 'bundled_codemirror', 'sources/utils', 'pgadmin.misc.explain', + 'pgadmin.user_management.current_user', 'sources/selection/grid_selector', 'sources/selection/active_cell_capture', 'sources/selection/clipboard', @@ -26,6 +27,7 @@ define('tools.querytool', [ 'sources/sqleditor/execute_query', 'sources/sqleditor/query_tool_http_error_handler', 'sources/sqleditor/filter_dialog', + 'sources/sqleditor/new_connection_dialog', 'sources/sqleditor/geometry_viewer', 'sources/sqleditor/history/history_collection.js', 'sources/sqleditor/history/query_history', @@ -53,8 +55,8 @@ define('tools.querytool', [ 'pgadmin.tools.user_management', ], function( gettext, url_for, $, jqueryui, jqueryui_position, _, alertify, pgAdmin, Backbone, codemirror, pgadminUtils, - pgExplain, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, - XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, + pgExplain, current_user, GridSelector, ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent, + XCellSelectionModel, setStagedRows, SqlEditorUtils, ExecuteQuery, httpErrorHandler, FilterHandler, newConnectionHandler, GeometryViewer, historyColl, queryHist, querySources, keyboardShortcuts, queryToolActions, queryToolNotifications, Datagrid, modifyAnimation, calculateQueryRunTime, callRenderAfterPoll, queryToolPref, queryTxnStatus, csrfToken, panelTitleFunc, @@ -98,6 +100,9 @@ define('tools.querytool', [ this.layout = opts.layout; this.set_server_version(opts.server_ver); this.trigger('pgadmin-sqleditor:view:initialised'); + this.connection_list = [ + {'server_group': null,'server': null, 'database': null, 'user': null, 'role': null, 'title': '<New Connection>'}, + ]; }, // Bind all the events @@ -163,6 +168,35 @@ define('tools.querytool', [ 'click .btn-macro': 'on_execute_macro', }, + render_connection: function(data_list) { + if(this.handler.is_query_tool) { + var dropdownElement = document.getElementById('connections-list'); + dropdownElement.innerHTML = ''; + data_list.forEach((option, index) => { + $('#connections-list').append('
  • '+ option.title +'
  • '); + + }); + var self = this; + $('.connection-list-item').click(function() { + self.get_connection_data(this); + }); + } else { + $('.conn-info-dd').hide(); + $('.editor-title').css({pointerEvents: 'none'}); + } + }, + + get_connection_data: function(event){ + var index = $(event).attr('data-index'); + var connection_details = this.connection_list[index]; + if(connection_details.server_group) { + this.on_change_connection(connection_details); + } else { + this.on_new_connection(); + } + + }, + reflectPreferences: function() { let self = this, browser = pgWindow.default.pgAdmin.Browser, @@ -213,6 +247,7 @@ define('tools.querytool', [ set_editor_title: function(title) { this.$el.find('.editor-title').text(title); + this.render_connection(this.connection_list); }, // This function is used to render the template. @@ -696,6 +731,8 @@ define('tools.querytool', [ pgBrowser.register_to_activity_listener(document, ()=>{ alertify.alert(gettext('Timeout'), gettext('Your session has timed out due to inactivity. Please close the window and login again.')); }); + + self.render_connection(self.connection_list); }, /* Regarding SlickGrid usage in render_grid function. @@ -1607,6 +1644,17 @@ define('tools.querytool', [ ); }, + on_new_connection: function() { + var self = this; + + // Trigger the show_filter signal to the SqlEditorController class + self.handler.trigger( + 'pgadmin-sqleditor:button:show_new_connection', + self, + self.handler + ); + }, + // Callback function for include filter button click. on_include_filter: function(ev) { var self = this; @@ -2070,6 +2118,83 @@ define('tools.querytool', [ queryToolActions.executeMacro(this.handler, macroId); }, + on_change_connection: function(connection_details, ref) { + let title = this.$el.find('.editor-title').html(); + if(connection_details['title'] != title) { + var self = this; + $.ajax({ + async: false, + url: url_for('datagrid.update_query_tool_connection', { + 'trans_id': self.transId, + 'sgid': connection_details['server_group'], + 'sid': connection_details['server'], + 'did': connection_details['database'], + }), + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(connection_details), + }) + .done(function(res) { + if(res.success) { + self.transId = res.data.tran_id; + self.handler.transId = res.data.tran_id; + self.handler.url_params = { + 'did': connection_details['database'], + 'is_query_tool': self.handler.url_params.is_query_tool, + 'server_type': self.handler.url_params.server_type, + 'sgid': connection_details['server_group'], + 'sid': connection_details['server'], + 'title': connection_details['title'], + }; + self.set_editor_title(self.handler.url_params.title); + self.handler.setTitle(self.handler.url_params.title); + alertify.success('connected successfully'); + if(ref){ + let connection_data = { + 'server_group': self.handler.url_params.sgid, + 'server': connection_details['server'], + 'database': connection_details['database'], + 'user': connection_details['user'], + 'title': connection_details['title'], + 'role': connection_details['role'], + 'password': connection_details['password'], + 'is_allow_new_connection': true, + }; + self.connection_list.unshift(connection_data); + self.render_connection(self.connection_list); + ref.close(); + } + } + return true; + }) + .fail(function(xhr) { + if(xhr.status == 428) { + alertify.connectServer('Connect to server', xhr.responseJSON.result, connection_details['server'], false); + } else { + alertify.error(xhr.responseJSON['errormsg']); + } + /*let url = url_for('sqleditor.connect_server_with_user', { + 'sid': newConnCollectionModel['server'], + 'usr': newConnCollectionModel['user'] + }); + $.ajax({ + async: false, + url: url, + headers: { + 'Cache-Control' : 'no-cache', + }, + }).done(function () { + Backform.Select2Control.prototype.onChange.apply(self, arguments); + response.server_list.forEach(function(obj){ + if(obj.id==self.model.changed.server) { + response.server_name = obj.name; + } + }); + }).fail(function(xhr){});*/ + + }); + } + }, }); @@ -2393,6 +2518,17 @@ define('tools.querytool', [ $('#btn-conn-status i').removeClass('obtaining-conn'); self.gridView.set_editor_title(_.unescape(url_params.title)); + let connection_data = { + 'server_group': self.gridView.handler.url_params.sgid, + 'server': self.gridView.handler.url_params.sid, + 'database': self.gridView.handler.url_params.did, + 'user': null, + 'role': null, + 'title': _.unescape(url_params.title), + 'is_allow_new_connection': false, + }; + self.gridView.connection_list.unshift(connection_data); + self.gridView.render_connection(self.gridView.connection_list); }; pgBrowser.Events.on('pgadmin:query_tool:connected:' + transId, afterConn); @@ -2487,6 +2623,7 @@ define('tools.querytool', [ self.on('pgadmin-sqleditor:button:save_file', self._save_file, self); self.on('pgadmin-sqleditor:button:deleterow', self._delete, self); self.on('pgadmin-sqleditor:button:show_filter', self._show_filter, self); + self.on('pgadmin-sqleditor:button:show_new_connection', self._show_new_connection, self); self.on('pgadmin-sqleditor:button:include_filter', self._include_filter, self); self.on('pgadmin-sqleditor:button:exclude_filter', self._exclude_filter, self); self.on('pgadmin-sqleditor:button:remove_filter', self._remove_filter, self); @@ -3696,7 +3833,6 @@ define('tools.querytool', [ } }; }, - // This function will show the filter in the text area. _show_filter: function() { let self = this, @@ -3711,7 +3847,19 @@ define('tools.querytool', [ } FilterHandler.dialog(self, reconnect); }, + // This function will show the new connection. + _show_new_connection: function() { + let self = this, + reconnect = false; + /* When server is disconnected and connected, connection is lost, + * To reconnect pass true + */ + if (arguments.length > 0 && arguments[arguments.length - 1] == 'connect') { + reconnect = true; + } + newConnectionHandler.dialog(self, reconnect); + }, // This function will include the filter by selection. _include_filter: function() { var self = this, diff --git a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss index fd1e5d35f..53f2449f6 100644 --- a/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss +++ b/web/pgadmin/tools/sqleditor/static/scss/_sqleditor.scss @@ -30,6 +30,19 @@ color: $sql-title-fg; } +.connection-info { + background: $sql-title-bg; + color: $sql-title-fg; + width:100%; + display: inherit; +} + +.conn-info-dd { + padding-top: 0.3em; + padding-left: 0.2em; + cursor: pointer; +} + #editor-panel { z-index: 0; diff --git a/web/pgadmin/tools/sqleditor/tests/test_new_connection_database.py b/web/pgadmin/tools/sqleditor/tests/test_new_connection_database.py new file mode 100644 index 000000000..e7990953b --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_new_connection_database.py @@ -0,0 +1,100 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.test_setup import config_data +from regression.python_test_utils import test_utils as utils + + +class TestNewConnectionDatabase(BaseTestGenerator): + """ This class will test new connection database. """ + API_URL = "/sqleditor/new_connection_database/" + scenarios = [ + ('New connection dialog', + dict( + url=API_URL, + is_positive_test=True, + mocking_required=False, + is_server_conn_required=False, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ('New connection dialog connect server', + dict( + url=API_URL, + is_positive_test=True, + mocking_required=False, + is_server_conn_required=True, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ('New connection dialog negative', + dict( + url=API_URL, + is_positive_test=False, + mocking_required=False, + is_server_conn_required=True, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ] + + def setUp(self): + self.content_type = 'html/json' + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + def get_database(self): + response = self.tester.get( + self.url + str(self.sgid) + '/' + str(self.sid), + content_type=self.content_type + ) + + return response + + def runTest(self): + if self.is_positive_test: + if self.is_server_conn_required: + self.server['password'] = self.server['db_password'] + self.tester.post( + '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.sid), + data=json.dumps(self.server), + content_type=self.content_type + ) + response = self.get_database() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + if self.is_server_conn_required: + self.server['password'] = self.server['db_password'] + self.tester.post( + '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.sid), + data=json.dumps(self.server), + content_type=self.content_type + ) + self.sid = 0 + response = self.get_database() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + self.assertEqual(actual_response_code, expected_response_code) diff --git a/web/pgadmin/tools/sqleditor/tests/test_new_connection_dialog.py b/web/pgadmin/tools/sqleditor/tests/test_new_connection_dialog.py new file mode 100644 index 000000000..75a47efca --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_new_connection_dialog.py @@ -0,0 +1,50 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import json +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.test_setup import config_data +from regression.python_test_utils import test_utils as utils + + +class TestNewConnectionDialog(BaseTestGenerator): + """ This class will test new connection dialog. """ + scenarios = [ + ('New connection dialog', + dict( + url="/sqleditor/new_connection_dialog/", + is_positive_test=True, + mocking_required=False, + is_connect_server=False, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ] + + def setUp(self): + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + def new_connection(self): + response = self.tester.get( + self.url + str(self.sgid) + '/' + str(self.sgid), + content_type='html/json' + ) + + return response + + def runTest(self): + if self.is_positive_test: + response = self.new_connection() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + self.assertEqual(actual_response_code, expected_response_code) diff --git a/web/pgadmin/tools/sqleditor/tests/test_new_connection_user.py b/web/pgadmin/tools/sqleditor/tests/test_new_connection_user.py new file mode 100644 index 000000000..7c69bbd1f --- /dev/null +++ b/web/pgadmin/tools/sqleditor/tests/test_new_connection_user.py @@ -0,0 +1,100 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.test_setup import config_data +from regression.python_test_utils import test_utils as utils + + +class TestNewConnectionUser(BaseTestGenerator): + """ This class will test new connection user. """ + API_URL = '/sqleditor/new_connection_user/' + scenarios = [ + ('New connection dialog', + dict( + url=API_URL, + is_positive_test=True, + mocking_required=False, + is_server_conn_required=False, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ('New connection dialog connect server', + dict( + url=API_URL, + is_positive_test=True, + mocking_required=False, + is_server_conn_required=True, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ('New connection dialog negative', + dict( + url=API_URL, + is_positive_test=False, + mocking_required=False, + is_server_conn_required=True, + test_data={}, + mock_data={}, + expected_data={ + "status_code": 200 + } + )), + ] + + def setUp(self): + self.content_type = 'html/json' + self.sid = parent_node_dict["server"][-1]["server_id"] + self.sgid = config_data['server_group'] + + def get_use(self): + response = self.tester.get( + self.url + str(self.sgid) + '/' + str(self.sid), + content_type=self.content_type + ) + + return response + + def runTest(self): + if self.is_positive_test: + if self.is_server_conn_required: + self.server['password'] = self.server['db_password'] + self.tester.post( + '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.sid), + data=json.dumps(self.server), + content_type=self.content_type + ) + response = self.get_use() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + else: + if self.is_server_conn_required: + self.server['password'] = self.server['db_password'] + self.tester.post( + '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.sid), + data=json.dumps(self.server), + content_type='html/json' + ) + self.sid = 0 + response = self.get_use() + actual_response_code = response.status_code + expected_response_code = self.expected_data['status_code'] + self.assertEqual(actual_response_code, expected_response_code) diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py index 010185f22..278881bad 100644 --- a/web/pgadmin/utils/constants.py +++ b/web/pgadmin/utils/constants.py @@ -44,3 +44,5 @@ ERROR_MSG_TRANS_ID_NOT_FOUND = gettext( # Role module constant ERROR_FETCHING_ROLE_INFORMATION = gettext( 'Error fetching role information from the database server.') + +ERROR_FETCHING_DATA = gettext('Unable to fetch data.') diff --git a/web/pgadmin/utils/driver/psycopg2/connection.py b/web/pgadmin/utils/driver/psycopg2/connection.py index 5295c5149..4f40e652b 100644 --- a/web/pgadmin/utils/driver/psycopg2/connection.py +++ b/web/pgadmin/utils/driver/psycopg2/connection.py @@ -21,7 +21,7 @@ import psycopg2 from flask import g, current_app from flask_babelex import gettext from flask_security import current_user -from pgadmin.utils.crypto import decrypt +from pgadmin.utils.crypto import decrypt, encrypt from psycopg2.extensions import encodings import config @@ -204,6 +204,45 @@ class Connection(BaseConnection): def __str__(self): return self.__repr__() + def _check_user_password(self, kwargs): + """ + Check user and password. + """ + password = None + encpass = None + is_update_password = True + + if 'user' in kwargs and kwargs['password']: + password = kwargs['password'] + kwargs.pop('password') + is_update_password = False + else: + encpass = kwargs['password'] if 'password' in kwargs else None + + return password, encpass, is_update_password + + def _decode_password(self, encpass, manager, password, crypt_key): + if encpass: + # Fetch Logged in User Details. + user = User.query.filter_by(id=current_user.id).first() + + if user is None: + return True, self.UNAUTHORIZED_REQUEST, password + + try: + password = decrypt(encpass, crypt_key) + # password is in bytes, for python3 we need it in string + if isinstance(password, bytes): + password = password.decode() + except Exception as e: + manager.stop_ssh_tunnel() + current_app.logger.exception(e) + return True, \ + _( + "Failed to decrypt the saved password.\nError: {0}" + ).format(str(e)) + return False, '', password + def connect(self, **kwargs): if self.conn: if self.conn.closed: @@ -212,11 +251,13 @@ class Connection(BaseConnection): return True, None pg_conn = None - password = None passfile = None manager = self.manager + crypt_key_present, crypt_key = get_crypt_key() + + password, encpass, is_update_password = self._check_user_password( + kwargs) - encpass = kwargs['password'] if 'password' in kwargs else None passfile = kwargs['passfile'] if 'passfile' in kwargs else None tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \ kwargs else '' @@ -231,38 +272,23 @@ class Connection(BaseConnection): if manager.use_ssh_tunnel == 1: manager.check_ssh_tunnel_alive() - if encpass is None: - encpass = self.password or getattr(manager, 'password', None) + if is_update_password: + if encpass is None: + encpass = self.password or getattr(manager, 'password', None) - self.password = encpass + self.password = encpass # Reset the existing connection password if self.reconnecting is not False: self.password = None - crypt_key_present, crypt_key = get_crypt_key() if not crypt_key_present: raise CryptKeyMissing() - if encpass: - # Fetch Logged in User Details. - user = User.query.filter_by(id=current_user.id).first() - - if user is None: - return False, self.UNAUTHORIZED_REQUEST - - try: - password = decrypt(encpass, crypt_key) - # password is in bytes, for python3 we need it in string - if isinstance(password, bytes): - password = password.decode() - except Exception as e: - manager.stop_ssh_tunnel() - current_app.logger.exception(e) - return False, \ - _( - "Failed to decrypt the saved password.\nError: {0}" - ).format(str(e)) + is_error, errmsg, password = self._decode_password(encpass, manager, + password, crypt_key) + if is_error: + return False, errmsg # If no password credential is found then connect request might # come from Query tool, ViewData grid, debugger etc tools. @@ -273,7 +299,10 @@ class Connection(BaseConnection): try: database = self.db - user = manager.user + if 'user' in kwargs and kwargs['user']: + user = kwargs['user'] + else: + user = manager.user conn_id = self.conn_id import os @@ -342,10 +371,10 @@ class Connection(BaseConnection): self.wasConnected = False raise e - if status: + if status and is_update_password: manager._update_password(encpass) else: - if not self.reconnecting: + if not self.reconnecting and is_update_password: self.wasConnected = False return status, msg @@ -363,7 +392,7 @@ class Connection(BaseConnection): else: self.conn.autocommit = True - def _set_role(self, manager, cur, conn_id): + def _set_role(self, manager, cur, conn_id, **kwargs): """ Set role :param manager: @@ -371,8 +400,18 @@ class Connection(BaseConnection): :param conn_id: :return: """ - if manager.role: - status = self._execute(cur, "SET ROLE TO %s", [manager.role]) + is_set_role = False + role = None + + if 'role' in kwargs and kwargs['role']: + is_set_role = True + role = kwargs['role'] + elif manager.role: + is_set_role = True + role = manager.role + + if is_set_role: + status = self._execute(cur, "SET ROLE TO %s", [role]) if status is not None: self.conn.close() @@ -386,7 +425,7 @@ class Connection(BaseConnection): msg=status ) ) - return False, \ + return True, \ _( "Failed to setup the role with error message:\n{0}" ).format(status) @@ -449,7 +488,7 @@ class Connection(BaseConnection): return False, status - is_error, errmsg = self._set_role(manager, cur, conn_id) + is_error, errmsg = self._set_role(manager, cur, conn_id, **kwargs) if is_error: return False, errmsg @@ -495,7 +534,7 @@ WHERE db.datname = current_database()""") if len(manager.db_info) == 1: manager.did = res['did'] - self._set_user_info(cur, manager) + self._set_user_info(cur, manager, **kwargs) self._set_server_type_and_password(kwargs, manager) @@ -503,7 +542,7 @@ WHERE db.datname = current_database()""") return True, None - def _set_user_info(self, cur, manager): + def _set_user_info(self, cur, manager, **kwargs): """ Set user info. :param cur: @@ -521,7 +560,7 @@ WHERE db.datname = current_database()""") WHERE rolname = current_user""") - if status is None: + if status is None and 'user' not in kwargs: manager.user_info = dict() if cur.rowcount > 0: manager.user_info = cur.fetchmany(1)[0]