mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-11-01 15:53:03 -06:00
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:
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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user