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 000000000..8cf7ae6aa Binary files /dev/null and b/docs/en_US/images/new_connection_dialog.png differ 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 000000000..7ca94374b Binary files /dev/null and b/docs/en_US/images/new_connection_options.png differ 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]