diff --git a/docs/en_US/clear_saved_passwords.rst b/docs/en_US/clear_saved_passwords.rst index 4ec6e8ed9..721196035 100644 --- a/docs/en_US/clear_saved_passwords.rst +++ b/docs/en_US/clear_saved_passwords.rst @@ -10,4 +10,10 @@ Use *Clear Saved Password* functionality to clear the saved password for the dat *Clear Saved Password* shows in the context menu for the selected server as well as under the *Object* menu on the top menu bar. +Use *Clear SSH Tunnel Password* functionality to clear the saved password of SSH Tunnel to connect to the database server. + +.. image:: images/clear_tunnel_password.png + +*Clear SSH Tunnel Password* shows in the context menu for the selected server as well as under the *Object* menu on the top menu bar. + **Note:** It will be enabled/visible when the password for the selected database server is already saved. \ No newline at end of file diff --git a/docs/en_US/connect_to_server.rst b/docs/en_US/connect_to_server.rst index 4acd2892f..797018cb3 100644 --- a/docs/en_US/connect_to_server.rst +++ b/docs/en_US/connect_to_server.rst @@ -14,6 +14,16 @@ Provide authentication information for the selected server: * Use the *Password* field to provide the password of the user that is associated with the defined server. * Check the box next to *Save Password* to instruct the server to save the password for future connections; if you save the password, you will not be prompted when reconnecting to the database server with this server definition. +In case of SSH Tunneling, *Connect to Server* dialog will prompt for SSH Tunnel and Database server passwords if not already saved. + +.. image:: images/connect_to_tunneled_server.png + :alt: Connect to server dialog + +Provide authentication information for the selected server: + + * Use the *Password* field to provide the password of the user that is associated with the defined server. + * Check the box next to respective *Save Password* to instruct the server to save the password for future connections; if you save the password, you will not be prompted when reconnecting to the database server with this server definition. + The pgAdmin client displays a message in a green status bar in the lower right corner when the server connects successfully. If you receive an error message while attempting a connection, verify that your network is allowing the pgAdmin host and the host of the database server to communicate. For detailed information about a specific error message, please see the :ref:`Connection Error ` help page. diff --git a/docs/en_US/images/clear_saved_password.png b/docs/en_US/images/clear_saved_password.png index 1b7bca304..28d6ef9ca 100644 Binary files a/docs/en_US/images/clear_saved_password.png and b/docs/en_US/images/clear_saved_password.png differ diff --git a/docs/en_US/images/clear_tunnel_password.png b/docs/en_US/images/clear_tunnel_password.png new file mode 100644 index 000000000..d4e0a305a Binary files /dev/null and b/docs/en_US/images/clear_tunnel_password.png differ diff --git a/docs/en_US/images/connect_to_tunneled_server.png b/docs/en_US/images/connect_to_tunneled_server.png new file mode 100644 index 000000000..001a1bf64 Binary files /dev/null and b/docs/en_US/images/connect_to_tunneled_server.png differ diff --git a/docs/en_US/images/object_menu.png b/docs/en_US/images/object_menu.png index 22d8fd25b..62d58e28e 100644 Binary files a/docs/en_US/images/object_menu.png and b/docs/en_US/images/object_menu.png differ diff --git a/docs/en_US/images/server_ssh_tunnel.png b/docs/en_US/images/server_ssh_tunnel.png index 059bfd4d8..74514c01c 100644 Binary files a/docs/en_US/images/server_ssh_tunnel.png and b/docs/en_US/images/server_ssh_tunnel.png differ diff --git a/docs/en_US/pgadmin_menu_bar.rst b/docs/en_US/pgadmin_menu_bar.rst index 003e6d231..5ad096624 100644 --- a/docs/en_US/pgadmin_menu_bar.rst +++ b/docs/en_US/pgadmin_menu_bar.rst @@ -28,38 +28,41 @@ Use the *File* menu to access the following options: The *Object* menu is context-sensitive. Use the *Object* menu to access the following options (in alphabetical order): -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| Option | Action | -+========================+==========================================================================================================================+ -| *Change Password...* | Click to open the :ref:`Change Password... ` dialog to change your password. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Clear Saved Password* | If you have saved the database server password, click to reset the saved password. | -| | Enable only when password is already saved and database server is disconnected. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Connect Server...* | Click to open the :ref:`Connect to Server ` dialog to establish a connection with a server. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Create* | Click *Create* to access a context menu that provides context-sensitive selections. | -| | Your selection opens a *Create* dialog for creating a new object. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Delete/Drop* | Click to delete the currently selected object from the server. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Disconnect Server...* | Click to refresh the currently selected object. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Drop Cascade* | Click to delete the currently selected object and all dependent objects from the server. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Properties...* | Click to review or modify the currently selected object's properties. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Refresh...* | Click to refresh the currently selected object. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Scripts* | Click to open the :ref:`Query tool ` to edit or view the selected script from the flyout menu. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Trigger(s)* | Click to *Disable* or *Enable* trigger(s) for the currently selected table. Options are displayed on the flyout menu. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *Truncate* | Click to remove all rows from a table (*Truncate*) or to remove all rows from a table and its child tables | -| | (*Truncate Cascade*). Options are displayed on the flyout menu. | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ -| *View Data* | Click to access a context menu that provides several options for viewing data (see below). | -+------------------------+--------------------------------------------------------------------------------------------------------------------------+ ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| Option | Action | ++=============================+==========================================================================================================================+ +| *Change Password...* | Click to open the :ref:`Change Password... ` dialog to change your password. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Clear Saved Password* | If you have saved the database server password, click to clear the saved password. | +| | Enable only when password is already saved. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Clear SSH Tunnel Password* | If you have saved the ssh tunnel password, click to clear the saved password. | +| | Enable only when password is already saved. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Connect Server...* | Click to open the :ref:`Connect to Server ` dialog to establish a connection with a server. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Create* | Click *Create* to access a context menu that provides context-sensitive selections. | +| | Your selection opens a *Create* dialog for creating a new object. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Delete/Drop* | Click to delete the currently selected object from the server. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Disconnect Server...* | Click to refresh the currently selected object. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Drop Cascade* | Click to delete the currently selected object and all dependent objects from the server. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Properties...* | Click to review or modify the currently selected object's properties. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Refresh...* | Click to refresh the currently selected object. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Scripts* | Click to open the :ref:`Query tool ` to edit or view the selected script from the flyout menu. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Trigger(s)* | Click to *Disable* or *Enable* trigger(s) for the currently selected table. Options are displayed on the flyout menu. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *Truncate* | Click to remove all rows from a table (*Truncate*) or to remove all rows from a table and its child tables | +| | (*Truncate Cascade*). Options are displayed on the flyout menu. | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ +| *View Data* | Click to access a context menu that provides several options for viewing data (see below). | ++-----------------------------+--------------------------------------------------------------------------------------------------------------------------+ **The Tools Menu** diff --git a/docs/en_US/release_notes_3_2.rst b/docs/en_US/release_notes_3_2.rst index ebc7ab5ce..9ddf97947 100644 --- a/docs/en_US/release_notes_3_2.rst +++ b/docs/en_US/release_notes_3_2.rst @@ -36,5 +36,6 @@ Bug fixes | `Bug #3458 `_ - pgAdmin4 should work with python 3.7. | `Bug #3468 `_ - Support SSH tunneling with keys that don't have a passphrase. | `Bug #3471 `_ - Ensure the SSH tunnel port number is honoured. +| `Bug #3511 `_ - Add support to save and clear SSH Tunnel password. | `Bug #3526 `_ - COST statement should not be automatically duplicated after creating trigger function. | `Bug #3527 `_ - View Data->Filtered Rows dialog should be displayed. \ No newline at end of file diff --git a/docs/en_US/server_dialog.rst b/docs/en_US/server_dialog.rst index 06aef629a..b02aecbe3 100644 --- a/docs/en_US/server_dialog.rst +++ b/docs/en_US/server_dialog.rst @@ -30,7 +30,7 @@ Use the fields in the *Connection* tab to configure a connection: * Use the *Maintenance database* field to specify the name of the initial database to which the client will connect. If you will be using pgAgent or adminpack objects, the pgAgent schema and adminpack objects should be installed on that database. * Use the *Username* field to specify the name of a role that will be used when authenticating with the server. * Use the *Password* field to provide a password that will be supplied when authenticating with the server. -* Check the box next to *Save password?* to instruct pgAdmin to save the password for future use. +* Check the box next to *Save password?* to instruct pgAdmin to save the password for future use. Use :ref:`Clear Saved Password ` to remove the saved password. * Use the *Role* field to specify the name of a role that has privileges that will be conveyed to the client after authentication with the server. This selection allows you to connect as one role, and then assume the permissions of this specified role after the connection is established. Note that the connecting role must be a member of the role specified. * Use the *Service* field to specify the service name. For more information, see `Section 33.16 of the Postgres documentation `_. @@ -73,7 +73,9 @@ not be able to connect directly. * Select the *Password* option to specify that pgAdmin will use a password for authentication to the SSH host. This is the default. * Select the *Identity file* to specify that pgAdmin will use a private key file when connecting. * If the SSH host is expecting a private key file for authentication, use the *Identity file* field to specify the location of the key file. -* If the SSH host is expecting a password, use the *Password/Passphrase* field to specify the password, or if an identity file is being used, the passphrase. +* If the SSH host is expecting a password of the user name or an identity file if being used, use the *Password* field to specify the password. +* Check the box next to *Save password?* to instruct pgAdmin to save the password for future use. Use :ref:`Clear SSH Tunnel Password ` to remove the saved password. + Click the *Advanced* tab to continue. diff --git a/web/config.py b/web/config.py index f853e6636..7346359b5 100644 --- a/web/config.py +++ b/web/config.py @@ -390,6 +390,9 @@ SESSION_SKIP_PATHS = [ # SSH Tunneling supports only for Python 2.7 and 3.4+ ########################################################################## SUPPORT_SSH_TUNNEL = True +# Allow SSH Tunnel passwords to be saved if the user chooses. +# Set to False to disable password saving. +ALLOW_SAVE_TUNNEL_PASSWORD = False ########################################################################## # Local config settings @@ -413,3 +416,4 @@ if (SUPPORT_SSH_TUNNEL is True and ((sys.version_info[0] == 2 and sys.version_info[1] < 7) or (sys.version_info[0] == 3 and sys.version_info[1] < 4))): SUPPORT_SSH_TUNNEL = False + ALLOW_SAVE_TUNNEL_PASSWORD = False diff --git a/web/migrations/versions/aa86fb60b73d_.py b/web/migrations/versions/aa86fb60b73d_.py new file mode 100644 index 000000000..eba07538f --- /dev/null +++ b/web/migrations/versions/aa86fb60b73d_.py @@ -0,0 +1,32 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +"""Added new column 'tunnel_password' to save the password of SSH Tunnel. + +Revision ID: aa86fb60b73d +Revises: 493cd3e39c0c +Create Date: 2018-07-26 11:19:50.879849 + +""" +from pgadmin.model import db + +# revision identifiers, used by Alembic. +revision = 'aa86fb60b73d' +down_revision = '493cd3e39c0c' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute( + 'ALTER TABLE server ADD COLUMN tunnel_password TEXT(64)' + ) + + +def downgrade(): + pass diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 570304238..1c26e0cab 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -139,7 +139,9 @@ class ServerModule(sg.ServerGroupPluginModule): in_recovery=in_recovery, wal_pause=wal_paused, is_password_saved=True if server.password is not None - else False + else False, + is_tunnel_password_saved=True + if server.tunnel_password is not None else False ) @property @@ -251,7 +253,8 @@ class ServerNode(PGChildNodeView): 'delete': 'pause_wal_replay', 'put': 'resume_wal_replay' }], 'check_pgpass': [{'get': 'check_pgpass'}], - 'clear_saved_password': [{'put': 'clear_saved_password'}] + 'clear_saved_password': [{'put': 'clear_saved_password'}], + 'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}] }) EXP_IP4 = "^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\." \ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\." \ @@ -362,7 +365,9 @@ class ServerNode(PGChildNodeView): in_recovery=in_recovery, wal_pause=wal_paused, is_password_saved=True if server.password is not None - else False + else False, + is_tunnel_password_saved=True + if server.tunnel_password is not None else False ) ) @@ -417,7 +422,9 @@ class ServerNode(PGChildNodeView): in_recovery=in_recovery, wal_pause=wal_paused, is_password_saved=True if server.password is not None - else False + else False, + is_tunnel_password_saved=True + if server.tunnel_password is not None else False ) ) @@ -787,6 +794,7 @@ class ServerNode(PGChildNodeView): conn = manager.connection() have_password = False + have_tunnel_password = False password = None passfile = None tunnel_password = '' @@ -801,6 +809,7 @@ class ServerNode(PGChildNodeView): db.session.commit() if 'tunnel_password' in data and data["tunnel_password"] != '': + have_tunnel_password = True tunnel_password = data['tunnel_password'] tunnel_password = \ encrypt(tunnel_password, current_user.password) @@ -828,6 +837,13 @@ class ServerNode(PGChildNodeView): setattr(server, 'password', password) db.session.commit() + if 'save_tunnel_password' in data and \ + data['save_tunnel_password'] and \ + have_tunnel_password and \ + config.ALLOW_SAVE_TUNNEL_PASSWORD: + setattr(server, 'tunnel_password', tunnel_password) + db.session.commit() + user = manager.user_info connected = True @@ -969,6 +985,9 @@ class ServerNode(PGChildNodeView): passfile = None tunnel_password = None save_password = False + save_tunnel_password = False + prompt_password = False + prompt_tunnel_password = False # Connect the Server manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid) @@ -977,10 +996,16 @@ class ServerNode(PGChildNodeView): # If server using SSH Tunnel if server.use_ssh_tunnel: if 'tunnel_password' not in data: - return self.get_response_for_password(server, 428) + if server.tunnel_password is None: + prompt_tunnel_password = True + else: + tunnel_password = server.tunnel_password else: - tunnel_password = data['tunnel_password'] if 'tunnel_password'\ - in data else '' + tunnel_password = data['tunnel_password'] \ + if 'tunnel_password'in data else '' + save_tunnel_password = data['save_tunnel_password'] \ + if tunnel_password and 'save_tunnel_password' in data \ + else False # Encrypt the password before saving with user's login # password key. try: @@ -995,9 +1020,7 @@ class ServerNode(PGChildNodeView): conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and server.password is None and \ server.passfile is None and server.service is None: - # Return the password template in case password is not - # provided, or password has not been saved earlier. - return self.get_response_for_password(server, 428) + prompt_password = True elif server.passfile and server.passfile != '': passfile = server.passfile else: @@ -1016,6 +1039,13 @@ class ServerNode(PGChildNodeView): current_app.logger.exception(e) return internal_server_error(errormsg=e.message) + # Check do we need to prompt for the database server or ssh tunnel + # password or both. Return the password template in case password is + # 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) + status = True try: status, errmsg = conn.connect( @@ -1027,7 +1057,7 @@ class ServerNode(PGChildNodeView): except Exception as e: current_app.logger.exception(e) return self.get_response_for_password( - server, 401, getattr(e, 'message', str(e))) + server, 401, True, True, getattr(e, 'message', str(e))) if not status: if hasattr(str, 'decode'): @@ -1037,8 +1067,8 @@ class ServerNode(PGChildNodeView): "Could not connected to server(#{0}) - '{1}'.\nError: {2}" .format(server.id, server.name, errmsg) ) - - return self.get_response_for_password(server, 401, errmsg) + return self.get_response_for_password(server, 401, True, + True, errmsg) else: if save_password and config.ALLOW_SAVE_PASSWORD: try: @@ -1054,6 +1084,19 @@ class ServerNode(PGChildNodeView): return internal_server_error(errormsg=e.message) + if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD: + try: + # Save the encrypted tunnel password. + setattr(server, 'tunnel_password', tunnel_password) + db.session.commit() + except Exception as e: + # Release Connection + current_app.logger.exception(e) + manager.release(database=server.maintenance_db) + conn = None + + return internal_server_error(errormsg=e.message) + current_app.logger.info('Connection Established for server: \ %s - %s' % (server.id, server.name)) # Update the recovery and wal pause option for the server @@ -1072,7 +1115,11 @@ class ServerNode(PGChildNodeView): 'db': manager.db, 'user': manager.user_info, 'in_recovery': in_recovery, - 'wal_pause': wal_paused + 'wal_pause': wal_paused, + 'is_password_saved': True if server.password is not None + else False, + 'is_tunnel_password_saved': True + if server.tunnel_password is not None else False, } ) @@ -1418,7 +1465,8 @@ class ServerNode(PGChildNodeView): ) return internal_server_error(errormsg=str(e)) - def get_response_for_password(self, server, status, errmsg=None): + def get_response_for_password(self, server, status, prompt_password=False, + prompt_tunnel_password=False, errmsg=None): if server.use_ssh_tunnel: return make_json_response( success=0, @@ -1431,7 +1479,9 @@ class ServerNode(PGChildNodeView): tunnel_host=server.tunnel_host, tunnel_identity_file=server.tunnel_identity_file, errmsg=errmsg, - _=gettext + _=gettext, + prompt_tunnel_password=prompt_tunnel_password, + prompt_password=prompt_password ) ) else: @@ -1478,9 +1528,45 @@ class ServerNode(PGChildNodeView): return make_json_response( success=1, - info=gettext("Clear saved password successfully."), + info=gettext("The saved password cleared successfully."), data={'is_password_saved': False} ) + def clear_sshtunnel_password(self, gid, sid): + """ + This function is used to remove sshtunnel password stored into + the pgAdmin's db file. + + :param gid: + :param sid: + :return: + """ + try: + server = Server.query.filter_by( + user_id=current_user.id, id=sid + ).first() + + if server is None: + return make_json_response( + success=0, + info=gettext("Could not find the required server.") + ) + + setattr(server, 'tunnel_password', None) + db.session.commit() + except Exception as e: + current_app.logger.error( + "Unable to clear ssh tunnel password." + "\nError: {0}".format(str(e)) + ) + + return internal_server_error(errormsg=str(e)) + + return make_json_response( + success=1, + info=gettext("The saved password cleared successfully."), + data={'is_tunnel_password_saved': False} + ) + ServerNode.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index c2e3b76ce..3520c71bd 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -119,6 +119,18 @@ define('pgadmin.node.server', [ } return false; }, + },{ + name: 'clear_sshtunnel_password', node: 'server', module: this, + applies: ['object', 'context'], callback: 'clear_sshtunnel_password', + label: gettext('Clear SSH Tunnel Password'), icon: 'fa fa-eraser', + priority: 12, + enable: function(node) { + if (node && node._type === 'server' && + node.is_tunnel_password_saved) { + return true; + } + return false; + }, }]); _.bindAll(this, 'connection_lost'); @@ -648,6 +660,46 @@ define('pgadmin.node.server', [ return false; }, + + /* Reset stored ssh tunnel password */ + clear_sshtunnel_password: function(args){ + var input = args || {}, + obj = this, + t = pgBrowser.tree, + i = input.item || t.selected(), + d = i && i.length == 1 ? t.itemData(i) : undefined; + + if (!d) + return false; + + Alertify.confirm( + gettext('Clear SSH Tunnel password'), + S( + gettext('Are you sure you want to clear the saved password of SSH Tunnel for server %s?') + ).sprintf(d.label).value(), + function() { + $.ajax({ + url: obj.generate_url(i, 'clear_sshtunnel_password', d, true), + method:'PUT', + }) + .done(function(res) { + if (res.success == 1) { + Alertify.success(res.info); + t.itemData(i).is_tunnel_password_saved=res.data.is_tunnel_password_saved; + } + else { + Alertify.error(res.info); + } + }) + .fail(function(xhr, status, error) { + Alertify.pgRespErrorNotify(xhr, error); + }); + }, + function() { return true; } + ); + + return false; + }, }, model: pgAdmin.Browser.Node.Model.extend({ defaults: { @@ -679,6 +731,7 @@ define('pgadmin.node.server', [ tunnel_identity_file: undefined, tunnel_password: undefined, tunnel_authentication: 0, + save_tunnel_password: false, connect_timeout: 0, }, // Default values! @@ -745,28 +798,13 @@ define('pgadmin.node.server', [ },{ id: 'save_password', controlLabel: gettext('Save password?'), type: 'checkbox', group: gettext('Connection'), mode: ['create'], - deps: ['connect_now', 'use_ssh_tunnel'], visible: function(model) { + deps: ['connect_now'], visible: function(model) { return model.get('connect_now') && model.isNew(); }, - disabled: function(model) { + disabled: function() { if (!current_user.allow_save_password) return true; - if (model.get('use_ssh_tunnel')) { - if (model.get('save_password')) { - Alertify.alert( - gettext('Stored Password'), - gettext('Database passwords cannot be stored when using SSH tunnelling. The \'Save password\' option has been turned off.') - ); - } - - setTimeout(function() { - model.set('save_password', false); - }, 10); - - return true; - } - return false; }, },{ @@ -918,6 +956,19 @@ define('pgadmin.node.server', [ disabled: function(model) { return !model.get('use_ssh_tunnel'); }, + }, { + id: 'save_tunnel_password', controlLabel: gettext('Save password?'), + type: 'checkbox', group: gettext('SSH Tunnel'), mode: ['create'], + deps: ['connect_now', 'use_ssh_tunnel'], visible: function(model) { + return model.get('connect_now') && model.isNew(); + }, + disabled: function(model) { + if (!current_user.allow_save_tunnel_password || + !model.get('use_ssh_tunnel')) + return true; + + return false; + }, }, { id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'), mode: ['properties', 'edit', 'create'], disabled: 'isConnected', diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html index ed0b68bae..61a7d5745 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html +++ b/web/pgadmin/browser/server_groups/servers/templates/servers/tunnel_password.html @@ -4,6 +4,7 @@
{{ errmsg }}
{% endif %} + {% if prompt_tunnel_password %} {% if tunnel_identity_file %}
{{ _('SSH Tunnel password for the identity file \'{0}\' to connect the server "{1}"').format(tunnel_identity_file, tunnel_host) }}
{% else %} @@ -14,15 +15,28 @@ + +   Save Password +
+ {% endif %} + {% if prompt_password %}
{{ _('Database server password for the user \'{0}\' to connect the server "{1}"').format(username, server_label) }}
+ +   Save Password +
+ {% endif %} diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py index cca9322b1..0be468d1b 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py +++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py @@ -20,13 +20,30 @@ class ServersWithSSHTunnelAddTestCase(BaseTestGenerator): ( 'Add server using SSH tunnel with password', dict( url='/browser/server/obj/', - with_password=True + with_password=True, + save_password=False, ) ), ( 'Add server using SSH tunnel with identity file', dict( url='/browser/server/obj/', - with_password=False + with_password=False, + save_password=False, + ) + ), + ( + 'Add server using SSH tunnel with password and saved it', dict( + url='/browser/server/obj/', + with_password=True, + save_password=True, + ) + ), + ( + 'Add server using SSH tunnel with identity file and save the ' + 'password', dict( + url='/browser/server/obj/', + with_password=False, + save_password=True, ) ), ] @@ -48,6 +65,9 @@ class ServersWithSSHTunnelAddTestCase(BaseTestGenerator): self.server['tunnel_authentication'] = 1 self.server['tunnel_identity_file'] = 'pkey_rsa' + if self.save_password: + self.server['tunnel_password'] = '123456' + response = self.tester.post( url, data=json.dumps(self.server), diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 0c414518e..f74033732 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy # ########################################################################## -SCHEMA_VERSION = 17 +SCHEMA_VERSION = 18 ########################################################################## # @@ -164,6 +164,7 @@ class Server(db.Model): nullable=False ) tunnel_identity_file = db.Column(db.String(64), nullable=True) + tunnel_password = db.Column(db.String(64), nullable=True) class ModulePreference(db.Model): diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py index ac6e2f4cf..5b4d322d6 100644 --- a/web/pgadmin/tools/user_management/__init__.py +++ b/web/pgadmin/tools/user_management/__init__.py @@ -150,7 +150,9 @@ def current_user_info(): else 'postgres' ), allow_save_password='true' if config.ALLOW_SAVE_PASSWORD - else 'false' + else 'false', + allow_save_tunnel_password='true' + if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false', ), status=200, mimetype="application/javascript" diff --git a/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js index 847ab4e54..4d2a4b1c5 100644 --- a/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js +++ b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js @@ -4,6 +4,7 @@ define('pgadmin.user_management.current_user', [], function() { 'email': '{{ email }}', 'is_admin': {{ is_admin }}, 'name': '{{ name }}', - 'allow_save_password': {{ allow_save_password }} + 'allow_save_password': {{ allow_save_password }}, + 'allow_save_tunnel_password': {{ allow_save_tunnel_password }} } }); diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py index ee07fdcd5..653504e35 100644 --- a/web/pgadmin/utils/driver/psycopg2/server_manager.py +++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py @@ -53,6 +53,7 @@ class ServerManager(object): self.server_type = None self.server_cls = None self.password = None + self.tunnel_password = None self.sid = server.id self.host = server.host @@ -84,6 +85,7 @@ class ServerManager(object): self.tunnel_username = server.tunnel_username self.tunnel_authentication = server.tunnel_authentication self.tunnel_identity_file = server.tunnel_identity_file + self.tunnel_password = server.tunnel_password else: self.use_ssh_tunnel = 0 self.tunnel_host = None @@ -91,6 +93,7 @@ class ServerManager(object): self.tunnel_username = None self.tunnel_authentication = None self.tunnel_identity_file = None + self.tunnel_password = None for con in self.connections: self.connections[con]._release() @@ -119,6 +122,17 @@ class ServerManager(object): else: res['password'] = self.password + if self.use_ssh_tunnel: + if hasattr(self, 'tunnel_password') and self.tunnel_password: + # If running under PY2 + if hasattr(self.tunnel_password, 'decode'): + res['tunnel_password'] = \ + self.tunnel_password.decode('utf-8') + else: + res['tunnel_password'] = str(self.tunnel_password) + else: + res['tunnel_password'] = self.tunnel_password + connections = res['connections'] = dict() for conn_id in self.connections: @@ -248,6 +262,9 @@ WHERE db.oid = {0}""".format(did)) try: if 'password' in data and data['password']: data['password'] = data['password'].encode('utf-8') + if 'tunnel_password' in data and data['tunnel_password']: + data['tunnel_password'] = \ + data['tunnel_password'].encode('utf-8') except Exception as e: current_app.logger.exception(e) @@ -265,6 +282,14 @@ WHERE db.oid = {0}""".format(did)) # auto_reconnect is true. if conn_info['wasConnected'] and conn_info['auto_reconnect']: try: + # Check SSH Tunnel needs to be created + if self.use_ssh_tunnel == 1 and not self.tunnel_created: + status, error = self.create_ssh_tunnel( + data['tunnel_password']) + + # Check SSH Tunnel is alive or not. + self.check_ssh_tunnel_alive() + conn.connect( password=data['password'], server_types=ServerType.types()