Skip to content

Commit

Permalink
Added keep-alive support for SSH sessions when connecting to a Postgr…
Browse files Browse the repository at this point in the history
…eSQL server via an SSH tunnel. pgadmin-org#7016
  • Loading branch information
akshay-joshi committed Dec 19, 2023
1 parent 4db13fa commit 4d83e79
Show file tree
Hide file tree
Showing 18 changed files with 140 additions and 81 deletions.
Binary file modified docs/en_US/images/server_ssh_tunnel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/en_US/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ notes for it.
.. toctree::
:maxdepth: 1

release_notes_8_2
release_notes_8_1
release_notes_8_0
release_notes_7_8
Expand Down
31 changes: 31 additions & 0 deletions docs/en_US/release_notes_8_2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
***********
Version 8.2
***********

Release date: 2024-01-11

This release contains a number of bug fixes and new features since the release of pgAdmin 4 v8.1.

Supported Database Servers
**************************
**PostgreSQL**: 12, 13, 14, 15, and 16

**EDB Advanced Server**: 12, 13, 14, 15, and 16

Bundled PostgreSQL Utilities
****************************
**psql**, **pg_dump**, **pg_dumpall**, **pg_restore**: 16.0


New features
************

| `Issue #7016 <https://github.com/pgadmin-org/pgadmin4/issues/7016>`_ - Added keep-alive support for SSH sessions when connecting to a PostgreSQL server via an SSH tunnel.
Housekeeping
************


Bug fixes
*********

4 changes: 4 additions & 0 deletions docs/en_US/server_dialog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ not be able to connect directly.
password for future use. Use
:ref:`Clear SSH Tunnel Password <clear_saved_passwords>` to remove the saved
password.
* Use the *Keep alive* field to specify interval in seconds defining the period
in which, if no data was sent over the connection, a ‘keepalive’ packet will
be sent (and ignored by the remote host). This can be useful to keep
connections alive over a NAT.


Click the *Advanced* tab to continue.
Expand Down
36 changes: 36 additions & 0 deletions web/migrations/versions/ec0f11f9a4e6_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2023, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################

"""
Revision ID: ec0f11f9a4e6
Revises: 44926ac97232
Create Date: 2023-12-18 17:09:34.499652
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'ec0f11f9a4e6'
down_revision = '44926ac97232'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('server', sa.Column('tunnel_keep_alive', sa.Integer(),
server_default='0'))
op.add_column('sharedserver', sa.Column('tunnel_keep_alive', sa.Integer(),
server_default='0'))


def downgrade():
# pgAdmin only upgrades, downgrade not implemented.
pass
7 changes: 7 additions & 0 deletions web/pgadmin/browser/server_groups/servers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ def create_shared_server(data, gid):
tunnel_username=None,
tunnel_authentication=0,
tunnel_identity_file=None,
tunnel_keep_alive=0,
shared=True,
connection_params=data.connection_params,
prepare_threshold=data.prepare_threshold
Expand Down Expand Up @@ -814,6 +815,7 @@ def update(self, gid, sid):
'tunnel_username': 'tunnel_username',
'tunnel_authentication': 'tunnel_authentication',
'tunnel_identity_file': 'tunnel_identity_file',
'tunnel_keep_alive': 'tunnel_keep_alive',
'shared': 'shared',
'shared_username': 'shared_username',
'kerberos_conn': 'kerberos_conn',
Expand Down Expand Up @@ -1061,6 +1063,7 @@ def properties(self, gid, sid):
tunnel_port = 22
tunnel_username = None
tunnel_authentication = False
tunnel_keep_alive = 0
connection_params = \
self.convert_connection_parameter(server.connection_params)

Expand All @@ -1070,6 +1073,7 @@ def properties(self, gid, sid):
tunnel_port = server.tunnel_port
tunnel_username = server.tunnel_username
tunnel_authentication = bool(server.tunnel_authentication)
tunnel_keep_alive = server.tunnel_keep_alive

response = {
'id': server.id,
Expand Down Expand Up @@ -1106,6 +1110,7 @@ def properties(self, gid, sid):
'tunnel_identity_file': server.tunnel_identity_file
if server.tunnel_identity_file else None,
'tunnel_authentication': tunnel_authentication,
'tunnel_keep_alive': tunnel_keep_alive,
'kerberos_conn': bool(server.kerberos_conn),
'gss_authenticated': manager.gss_authenticated,
'gss_encrypted': manager.gss_encrypted,
Expand Down Expand Up @@ -1201,6 +1206,7 @@ def create(self, gid):
tunnel_authentication=1 if data.get('tunnel_authentication',
False) else 0,
tunnel_identity_file=data.get('tunnel_identity_file', None),
tunnel_keep_alive=data.get('tunnel_keep_alive', 0),
shared=data.get('shared', None),
shared_username=data.get('shared_username', None),
passexec_cmd=data.get('passexec_cmd', None),
Expand Down Expand Up @@ -2091,6 +2097,7 @@ def get_response_for_password(self, server, status, prompt_password=False,
"tunnel_username": server.tunnel_username,
"tunnel_host": server.tunnel_host,
"tunnel_identity_file": server.tunnel_identity_file,
"tunnel_keep_alive": server.tunnel_keep_alive,
"errmsg": errmsg,
"service": server.service,
"prompt_tunnel_password": prompt_tunnel_password,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,43 +494,6 @@ export default class SubscriptionSchema extends BaseUISchema{
setError('pub', null);
}

if (state.use_ssh_tunnel) {
if(isEmptyString(state.tunnel_host)) {
errmsg = gettext('SSH Tunnel host must be specified.');
setError('tunnel_host', errmsg);
return true;
} else {
setError('tunnel_host', null);
}

if(isEmptyString(state.tunnel_port)) {
errmsg = gettext('SSH Tunnel port must be specified.');
setError('tunnel_port', errmsg);
return true;
} else {
setError('tunnel_port', null);
}

if(isEmptyString(state.tunnel_username)) {
errmsg = gettext('SSH Tunnel username must be specified.');
setError('tunnel_username', errmsg);
return true;
} else {
setError('tunnel_username', null);
}

if (state.tunnel_authentication) {
if(isEmptyString(state.tunnel_identity_file)) {
errmsg = gettext('SSH Tunnel identity file must be specified.');
setError('tunnel_identity_file', errmsg);
return true;
} else {
setError('tunnel_identity_file', null);
}
}
}


return false;
}

Expand Down
18 changes: 18 additions & 0 deletions web/pgadmin/browser/server_groups/servers/static/js/server.ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default class ServerSchema extends BaseUISchema {
tunnel_identity_file: undefined,
tunnel_password: undefined,
tunnel_authentication: false,
tunnel_keep_alive: 0,
save_tunnel_password: false,
connection_string: undefined,
connection_params: [
Expand Down Expand Up @@ -327,6 +328,15 @@ export default class ServerSchema extends BaseUISchema {
return (!current_user.allow_save_tunnel_password || !state.use_ssh_tunnel);
},
},
{
id: 'tunnel_keep_alive', label: gettext('Keep alive (seconds)'),
type: 'int', group: gettext('SSH Tunnel'), min: 0,
mode: ['properties', 'edit', 'create'], deps: ['use_ssh_tunnel'],
disabled: function(state) {
return !state.use_ssh_tunnel;
},
readonly: obj.isConnected,
},
{
id: 'db_res', label: gettext('DB restriction'), type: 'select', group: gettext('Advanced'),
options: [],
Expand Down Expand Up @@ -436,6 +446,14 @@ export default class ServerSchema extends BaseUISchema {
setError('tunnel_identity_file', null);
}
}

if(isEmptyString(state.tunnel_keep_alive)) {
errmsg = gettext('Keep alive must be specified. Specify 0 for no keep alive.');
setError('tunnel_keep_alive', errmsg);
return true;
} else {
setError('tunnel_keep_alive', null);
}
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
"tunnel_port": 22,
"tunnel_username": "user",
"tunnel_authentication": 1,
"tunnel_identity_file": "pkey_rsa"
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 5
},
"mocking_required": false,
"mock_data": {},
Expand All @@ -74,7 +75,8 @@
"tunnel_port": 22,
"tunnel_username": "user",
"tunnel_authentication": 1,
"tunnel_identity_file": "pkey_rsa"
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 0
},
"mocking_required": false,
"mock_data": {},
Expand All @@ -95,7 +97,8 @@
"tunnel_port": 22,
"tunnel_username": "user",
"tunnel_authentication": 0,
"tunnel_password": "123456"
"tunnel_password": "123456",
"tunnel_keep_alive": 0
},
"mocking_required": false,
"mock_data": {},
Expand All @@ -117,7 +120,8 @@
"tunnel_username": "user",
"tunnel_authentication": 1,
"tunnel_identity_file": "pkey_rsa",
"tunnel_password": "123456"
"tunnel_password": "123456",
"tunnel_keep_alive": 0
},
"mocking_required": false,
"mock_data": {},
Expand Down Expand Up @@ -574,6 +578,7 @@
"tunnel_authentication": 1,
"tunnel_password": "user123",
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 0,
"service": null,
"server_info": {
"id": 1,
Expand Down Expand Up @@ -615,6 +620,7 @@
"tunnel_authentication": 1,
"tunnel_password": "",
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 0,
"service": null,
"server_info": {
"id": 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def runTest(self):
self.server['tunnel_host'] = self.test_data['tunnel_host']
self.server['tunnel_port'] = self.test_data['tunnel_port']
self.server['tunnel_username'] = self.test_data['tunnel_username']
self.server['tunnel_keep_alive'] = \
self.test_data['tunnel_keep_alive']

if self.with_password:
self.server['tunnel_authentication'] = self.test_data[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def get_ssh_tunnel(self):
self.server.tunnel_host = '127.0.0.1'
self.server.tunnel_port = 22
self.server.tunnel_username = 'user'
self.server.tunnel_keep_alive = 0
if hasattr(self, 'with_password') and self.with_password:
self.server.tunnel_authentication = 0
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class TestMockServer():
def __init__(self, name, id, username, use_ssh_tunnel,
tunnel_host, tunnel_port,
tunnel_username, tunnel_authentication,
tunnel_identity_file, tunnel_password, service):
tunnel_identity_file, tunnel_password,
tunnel_keep_alive, service):
self.name = name
self.id = id
self.username = username
Expand All @@ -71,6 +72,7 @@ def __init__(self, name, id, username, use_ssh_tunnel,
self.tunnel_identity_file = \
tunnel_identity_file
self.tunnel_password = tunnel_password
self.tunnel_keep_alive = tunnel_keep_alive
self.service = service
self.shared = None

Expand All @@ -85,6 +87,7 @@ def __init__(self, name, id, username, use_ssh_tunnel,
self.mock_data['tunnel_authentication'],
self.mock_data['tunnel_identity_file'],
self.mock_data['tunnel_password'],
self.mock_data['tunnel_keep_alive'],
self.mock_data['service'],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"tunnel_authentication": 1,
"tunnel_password": "user123",
"tunnel_identity_file": "pkey_rsa",
"tunnel_keep_alive": 0,
"service": null,
"fgcolor":"#B6D7A8",
"bgcolor": "#0C343D",
Expand Down
4 changes: 3 additions & 1 deletion web/pgadmin/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
#
##########################################################################

SCHEMA_VERSION = 38
SCHEMA_VERSION = 39

##########################################################################
#
Expand Down Expand Up @@ -201,6 +201,7 @@ class Server(db.Model):
)
tunnel_identity_file = db.Column(db.String(64), nullable=True)
tunnel_password = db.Column(PgAdminDbBinaryString())
tunnel_keep_alive = db.Column(db.Integer(), nullable=True)
shared = db.Column(db.Boolean(), nullable=False)
shared_username = db.Column(db.String(64), nullable=True)
kerberos_conn = db.Column(db.Boolean(), nullable=False, default=0)
Expand Down Expand Up @@ -413,6 +414,7 @@ class SharedServer(db.Model):
)
tunnel_identity_file = db.Column(db.String(64), nullable=True)
tunnel_password = db.Column(PgAdminDbBinaryString())
tunnel_keep_alive = db.Column(db.Integer(), nullable=True)
shared = db.Column(db.Boolean(), nullable=False)
connection_params = db.Column(MutableDict.as_mutable(types.JSON))
prepare_threshold = db.Column(db.Integer(), nullable=True)
Expand Down
2 changes: 1 addition & 1 deletion web/pgadmin/utils/driver/psycopg3/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1465,7 +1465,7 @@ async def _close_conn(conn):
def _wait(self, conn):
pass # This function is empty

def _wait_timeout(self, conn):
def _wait_timeout(self, conn, time):
pass # This function is empty

def poll(self, formatted_exception_msg=False, no_result=False):
Expand Down
Loading

0 comments on commit 4d83e79

Please sign in to comment.