Added option to skip the password dialog when using an identity file. #6996

1. Don't ask for the SSH Tunnel or Database server password if it is already saved.
2. Modified the generic error message from the SSH Tunnel library.
This commit is contained in:
Akshay Joshi authored and GitHub committed 2025-10-07 13:13:37 +05:30
1 parent b30520adc0
commit f3567518d3
13 files changed
+142 -27

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 94 KiB

+3 -1
View File
@@ -8,7 +8,7 @@ This release contains a number of bug fixes and new features since the release o
Supported Database Servers
**************************
**PostgreSQL**: 13, 14, 15, 16 and 17
**PostgreSQL**: 13, 14, 15, 16, 17 and 18
**EDB Advanced Server**: 13, 14, 15, 16 and 17
@@ -20,8 +20,10 @@ Bundled PostgreSQL Utilities
New features
************
| `Issue #6385 <https://github.com/pgadmin-org/pgadmin4/issues/6385>`_ - Add support of DEPENDS/NO DEPENDS ON EXTENSION for ALTER FUNCTION.
| `Issue #6394 <https://github.com/pgadmin-org/pgadmin4/issues/6394>`_ - Added "MULTIRANGE_TYPE_NAME" option while creating a Range Type.
| `Issue #6395 <https://github.com/pgadmin-org/pgadmin4/issues/6395>`_ - Added "SUBSCRIPT" option while creating a External Type.
| `Issue #6996 <https://github.com/pgadmin-org/pgadmin4/issues/6996>`_ - Added option to skip the password dialog when using an identity file.
| `Issue #8932 <https://github.com/pgadmin-org/pgadmin4/issues/8932>`_ - Added 'failover' and 'two_phase' parameter support in CREATE/ALTER SUBSCRIPTION for PostgreSQL v17+.
Housekeeping
+3
View File
@@ -177,6 +177,9 @@ not be able to connect directly.
*Identity file* field to specify the location of the key file.
* 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 *Prompt for password?* to to have pgAdmin prompt for
a password if the identity file includes one. This setting applies only when
using an identity file, which may or may not require a password.
* Check the box next to *Save password?* to instruct pgAdmin to save the
password for future use. Use
:ref:`Clear SSH Tunnel Password <clear_saved_passwords>` to remove the saved
+42
View File
@@ -0,0 +1,42 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2025, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""
Revision ID: efbbe5d5862f
Revises: e6ed5dac37c2
Create Date: 2025-09-29 18:40:30.248908
"""
from alembic import op, context
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'efbbe5d5862f'
down_revision = 'e6ed5dac37c2'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table(
"server",
table_kwargs={'sqlite_autoincrement': True}) as batch_op:
batch_op.add_column(sa.Column('tunnel_prompt_password',
sa.Integer(), server_default='0'))
with op.batch_alter_table(
"sharedserver",
table_kwargs={'sqlite_autoincrement': True}) as batch_op:
batch_op.add_column(sa.Column('tunnel_prompt_password',
sa.Integer(), server_default='0'))
def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
@@ -167,6 +167,7 @@ class ServerModule(sg.ServerGroupPluginModule):
server.tunnel_password = sharedserver.tunnel_password
server.save_password = sharedserver.save_password
server.tunnel_identity_file = sharedserver.tunnel_identity_file
server.tunnel_prompt_password = sharedserver.tunnel_prompt_password
if hasattr(server, 'connection_params') and \
hasattr(sharedserver, 'connection_params') and \
'passfile' in server.connection_params and \
@@ -413,6 +414,7 @@ class ServerModule(sg.ServerGroupPluginModule):
tunnel_authentication=0,
tunnel_identity_file=None,
tunnel_keep_alive=0,
tunnel_prompt_password=0,
shared=True,
connection_params=data.connection_params,
prepare_threshold=data.prepare_threshold
@@ -790,6 +792,7 @@ class ServerNode(PGChildNodeView):
'tunnel_username': 'tunnel_username',
'tunnel_authentication': 'tunnel_authentication',
'tunnel_identity_file': 'tunnel_identity_file',
'tunnel_prompt_password': 'tunnel_prompt_password',
'tunnel_keep_alive': 'tunnel_keep_alive',
'shared': 'shared',
'shared_username': 'shared_username',
@@ -1086,6 +1089,8 @@ class ServerNode(PGChildNodeView):
'tunnel_username': tunnel_username,
'tunnel_identity_file': server.tunnel_identity_file
if server.tunnel_identity_file else None,
'tunnel_prompt_password': server.tunnel_prompt_password
if server.tunnel_identity_file else 0,
'tunnel_authentication': tunnel_authentication,
'tunnel_keep_alive': tunnel_keep_alive,
'kerberos_conn': bool(server.kerberos_conn),
@@ -1212,6 +1217,8 @@ class ServerNode(PGChildNodeView):
tunnel_authentication=1 if data.get('tunnel_authentication',
False) else 0,
tunnel_identity_file=data.get('tunnel_identity_file', None),
tunnel_prompt_password=1 if data.get('tunnel_prompt_password',
True) else 0,
tunnel_keep_alive=data.get('tunnel_keep_alive', 0),
shared=data.get('shared', None),
shared_username=data.get('shared_username', None),
@@ -1419,6 +1426,19 @@ class ServerNode(PGChildNodeView):
}
)
def is_prompt_tunnel_password(self, server):
"""
This function will check whether to prompt tunnel password or not.
"""
prompt_tunnel_password = True
# In case of identity file check the value of tunnel_prompt_password.
if server.tunnel_password is not None or \
(server.tunnel_identity_file is not None and
server.tunnel_prompt_password != 1):
prompt_tunnel_password = False
return prompt_tunnel_password
def connect(self, gid, sid, is_qt=False, server=None):
"""
Connect the Server and return the connection object.
@@ -1502,11 +1522,12 @@ class ServerNode(PGChildNodeView):
# If server using SSH Tunnel
if server.use_ssh_tunnel:
if 'tunnel_password' not in data:
if server.tunnel_password is None:
prompt_tunnel_password = True
else:
tunnel_password = server.tunnel_password
if 'tunnel_password' not in data and \
server.tunnel_password is None:
prompt_tunnel_password = self.is_prompt_tunnel_password(server)
elif 'tunnel_password' not in data and \
server.tunnel_password is not None:
tunnel_password = server.tunnel_password
else:
tunnel_password = data['tunnel_password'] \
if 'tunnel_password' in data else ''
@@ -1562,6 +1583,10 @@ class ServerNode(PGChildNodeView):
return self.get_response_for_password(
server, 428, prompt_password, prompt_tunnel_password)
# Check whether to prompt for the tunnel password in case if
# password is saved in server object or in data.
prompt_tunnel_password = self.is_prompt_tunnel_password(server)
try:
status, errmsg = conn.connect(
password=password,
@@ -1571,7 +1596,7 @@ class ServerNode(PGChildNodeView):
)
except Exception as e:
return self.get_response_for_password(
server, 401, True, True,
server, 401, not server.save_password, prompt_tunnel_password,
getattr(e, 'message', str(e)))
if not status:
@@ -1583,7 +1608,8 @@ class ServerNode(PGChildNodeView):
return internal_server_error(errmsg)
return self.get_response_for_password(
server, 401, True, True, errmsg)
server, 401, not server.save_password,
prompt_tunnel_password, errmsg)
else:
if save_password and config.ALLOW_SAVE_PASSWORD:
try:
@@ -211,6 +211,7 @@ export default class ServerSchema extends BaseUISchema {
tunnel_port: 22,
tunnel_username: undefined,
tunnel_identity_file: undefined,
tunnel_prompt_password: false,
tunnel_password: undefined,
tunnel_authentication: false,
tunnel_keep_alive: 0,
@@ -496,7 +497,22 @@ export default class ServerSchema extends BaseUISchema {
maxLength: null
},
readonly: obj.isConnected,
}, {
},
{
id: 'tunnel_prompt_password', label: gettext('Prompt for password?'),
type: 'switch', group: gettext('SSH Tunnel'), mode: ['properties', 'edit', 'create'],
deps: ['tunnel_authentication', 'use_ssh_tunnel'],
depChange: (state)=>{
if (!state.tunnel_authentication) {
return {tunnel_prompt_password: false};
}
},
disabled: function(state) {
return !state.tunnel_authentication || !state.use_ssh_tunnel;
},
helpMessage: gettext('This setting applies only when using an identity file. An identity file may or may not have a password. If set to true the system will prompt for the password.')
},
{
id: 'save_tunnel_password', label: gettext('Save password?'),
type: 'switch', group: gettext('SSH Tunnel'), mode: ['create'],
deps: ['connect_now', 'use_ssh_tunnel'],
@@ -633,6 +633,7 @@
"tunnel_password": "user123",
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 0,
"tunnel_prompt_password": 0,
"service": null,
"server_info": {
"id": 1,
@@ -77,6 +77,7 @@ class ServersSSHConnectTestCase(BaseTestGenerator):
self.service = service
self.save_password = 0
self.shared = None
self.tunnel_prompt_password = 0
mock_server_obj = TestMockServer(
self.mock_data['name'],
+11 -1
View File
@@ -33,7 +33,7 @@ import config
#
##########################################################################
SCHEMA_VERSION = 47
SCHEMA_VERSION = 48
##########################################################################
#
@@ -246,6 +246,11 @@ class Server(db.Model):
nullable=False
)
tunnel_identity_file = db.Column(db.String(64), nullable=True)
tunnel_prompt_password = db.Column(
db.Integer(), db.CheckConstraint(
'tunnel_prompt_password >= 0 AND tunnel_prompt_password <= 1'),
nullable=False
)
tunnel_password = db.Column(PgAdminDbBinaryString())
tunnel_keep_alive = db.Column(db.Integer(), nullable=True, default=0)
shared = db.Column(db.Boolean(), nullable=False)
@@ -483,6 +488,11 @@ class SharedServer(db.Model):
nullable=False
)
tunnel_identity_file = db.Column(db.String(64), nullable=True)
tunnel_prompt_password = db.Column(
db.Integer(), db.CheckConstraint(
'tunnel_prompt_password >= 0 AND tunnel_prompt_password <= 1'),
nullable=False
)
tunnel_password = db.Column(PgAdminDbBinaryString())
tunnel_keep_alive = db.Column(db.Integer(), nullable=True)
shared = db.Column(db.Boolean(), nullable=False)
+1
View File
@@ -36,6 +36,7 @@
"TunnelPort": "22",
"TunnelUsername": "username",
"TunnelAuthentication": 0,
"TunnelPromptPassword": 0,
"PasswordExecCommand": "echo 'test'",
"PasswordExecExpiration": 100
}
@@ -109,21 +109,23 @@ export default function ConnectServerContent({closeModal, data, onOK, setHeight,
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={()=>{
closeModal();
}} >{gettext('Cancel')}</DefaultButton>
<PrimaryButton ref={okBtnRef} data-test="save" startIcon={<CheckRoundedIcon />} onClick={()=>{
let postFormData = new FormData();
if(data.prompt_tunnel_password) {
postFormData.append('tunnel_password', formData.tunnel_password);
formData.save_tunnel_password &&
postFormData.append('save_tunnel_password', formData.save_tunnel_password);
}
if(data.prompt_password) {
postFormData.append('password', formData.password);
formData.save_password &&
postFormData.append('save_password', formData.save_password);
}
onOK?.(postFormData);
closeModal();
}} >{gettext('OK')}</PrimaryButton>
{(data.prompt_password || data.prompt_tunnel_password) && <>
<PrimaryButton ref={okBtnRef} data-test="save" startIcon={<CheckRoundedIcon />} onClick={()=>{
let postFormData = new FormData();
if(data.prompt_tunnel_password) {
postFormData.append('tunnel_password', formData.tunnel_password);
formData.save_tunnel_password &&
postFormData.append('save_tunnel_password', formData.save_tunnel_password);
}
if(data.prompt_password) {
postFormData.append('password', formData.password);
formData.save_password &&
postFormData.append('save_password', formData.save_password);
}
onOK?.(postFormData);
closeModal();
}} >{gettext('OK')}</PrimaryButton>
</>}
</ModalFooter>
</ModalContent>
);
+5
View File
@@ -525,6 +525,8 @@ def dump_database_servers(output_file, selected_servers,
server.tunnel_authentication)
add_value(attr_dict, "TunnelIdentityFile",
server.tunnel_identity_file)
add_value(attr_dict, "TunnelPromptPassword",
server.tunnel_prompt_password)
add_value(attr_dict, "TunnelKeepAlive",
server.tunnel_keep_alive)
add_value(attr_dict, "KerberosAuthentication",
@@ -773,6 +775,9 @@ def load_database_servers(input_file, selected_servers,
new_server.tunnel_identity_file = \
obj.get("TunnelIdentityFile", None)
new_server.tunnel_prompt_password = \
obj.get("TunnelPromptPassword", 0)
new_server.tunnel_keep_alive = \
obj.get("TunnelKeepAlive", None)
@@ -99,6 +99,7 @@ class ServerManager(object):
if server.tunnel_authentication is None \
else server.tunnel_authentication
self.tunnel_identity_file = server.tunnel_identity_file
self.tunnel_prompt_password = server.tunnel_prompt_password
self.tunnel_password = server.tunnel_password
self.tunnel_keep_alive = server.tunnel_keep_alive
else:
@@ -108,6 +109,7 @@ class ServerManager(object):
self.tunnel_username = None
self.tunnel_authentication = None
self.tunnel_identity_file = None
self.tunnel_prompt_password = 0
self.tunnel_password = None
self.tunnel_keep_alive = 0
@@ -610,8 +612,12 @@ WHERE db.oid = {0}""".format(did))
self.tunnel_created = True
except BaseSSHTunnelForwarderError as e:
current_app.logger.exception(e)
return False, gettext("Failed to create the SSH tunnel.\n"
"Error: {0}").format(str(e))
return False, gettext(
"Failed to create the SSH tunnel. Possible causes:\n"
"1. Enter the correct tunnel password (Clear saved password "
"if it has changed).\n 2. If using an identity file that "
"requires a password, enable “Prompt for Password?” in the "
"server dialog. \n 3. Verify the host address.")
# Update the port to communicate locally
self.local_bind_port = self.tunnel_object.local_bind_port