Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 115 additions & 5 deletions web/pgadmin/utils/driver/psycopg3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,103 @@ def _restore_connections_from_session(self):
session['__pgsql_server_managers'].copy()
servers = get_user_server_query().filter(
Server.is_adhoc == 0)
pga_user = self._current_pga_user()
for server in servers:
manager = managers[str(server.id)] = \
ServerManager(server)
manager.pga_user = pga_user
# Suppress passexec for non-owners of shared
# servers — it runs commands on the client
# machine and must not inherit the owner's.
if config.SERVER_MODE and server.shared and \
server.user_id != current_user.id:
manager.passexec = None
if server.id in session_managers:
manager._restore(
session_managers[server.id])
manager.update_session()
saved = session_managers[server.id]
if self._saved_state_is_stale(
saved, server, pga_user):
# The persisted blob was serialized under
# this numeric id by whatever Server row
# held it before (e.g. the configuration
# database was reset or restored without
# restarting pgAdmin), so it no longer
# describes this row. Restoring it would
# hand the new row the previous row's
# password/connection state. The same goes
# for state saved by a different pgAdmin
# user on this browser session. Drop it and
# let the manager start clean.
manager.update_session()
else:
manager._restore(saved)
manager.update_session()
return managers

return {}

@staticmethod
def _current_pga_user():
"""
The fs_uniquifier of the logged-in pgAdmin user, which cached and
serialized ServerManager state is bound to. Unlike User.id it is
random, so it is not reused when the configuration database is
reset and a new user is created.
"""
return getattr(current_user, 'fs_uniquifier', None)

@staticmethod
def _saved_state_is_stale(saved, server_data, pga_user=None):
"""
Same identity check as _manager_is_stale, applied to the
serialized ServerManager state carried across worker
restarts/new sessions in the Flask session
('__pgsql_server_managers'), before it is restored onto a
manager that was just built fresh from the current Server row.
Without this, a reused server id would have its old serialized
password/connections restored onto the new row on the very
first request, before any manager exists to run
_manager_is_stale against.

The state is also stale if it was saved by a different pgAdmin
user: nothing rotates the session id at login, so a new user
logging in on the same browser session must not inherit the
previous user's password or connections, even for a server with
identical connection details.
"""
return (
saved.get('pga_user') != pga_user or
saved.get('host') != server_data.host or
saved.get('port') != server_data.port or
saved.get('db') != server_data.maintenance_db or
saved.get('user') != server_data.username or
saved.get('service') != server_data.service or
saved.get('tunnel_host') != server_data.tunnel_host
)

@staticmethod
def _manager_is_stale(manager, server_data, pga_user=None):
"""
A cached manager is normally kept in sync with edits to its
Server row via explicit manager.update() calls from the
server-edit endpoints. It can still go stale in place if the
row itself was swapped out from under it, e.g. a numeric
server id reused by an unrelated row after the configuration
database was reset or restored without restarting pgAdmin, so
compare against what actually identifies the target rather
than trusting the id match alone. It is also stale if it was
built for a different pgAdmin user on the same browser session
(see _saved_state_is_stale).
"""
return (
getattr(manager, 'pga_user', None) != pga_user or
manager.host != server_data.host or
manager.port != server_data.port or
manager.db != server_data.maintenance_db or
manager.user != server_data.username or
manager.service != server_data.service or
manager.tunnel_host != server_data.tunnel_host
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def connection_manager(self, sid=None):
"""
connection_manager(...)
Expand Down Expand Up @@ -143,15 +223,45 @@ def connection_manager(self, sid=None):
managers = self.managers[session.sid]
if str(sid) in managers:
manager = managers[str(sid)]
pga_user = self._current_pga_user()
with connection_restore_lock:
manager._restore_connections()
manager.update_session()
if self._manager_is_stale(
manager, server_data, pga_user):
# The id has been reused by an unrelated Server
# row (e.g. the configuration database was reset
# or restored without restarting pgAdmin), so the
# cached manager still points at whatever server
# it was originally built from. Drop it rather
# than report a live connection to a server that,
# from this row's perspective, was never opened.
# The same applies to a manager built for another
# pgAdmin user on this browser session.
manager.release()
manager.update(server_data)
manager.pga_user = pga_user
else:
manager._restore_connections()
manager.update_session()
# Identity (host/port/db/user/service/tunnel)
# still matches, so the live connection is kept,
# but access-control-relevant metadata such as
# shared/ownership is not part of that identity
# check and manager.update() was skipped above -
# refresh it here too, otherwise a row whose
# sharing/ownership changed via the same reused-id
# path could keep serving the previous owner's
# passexec to a new, non-owning user.
manager.shared = server_data.shared
if config.SERVER_MODE and server_data.shared and \
server_data.user_id != current_user.id:
manager.passexec = None

managers['pinged'] = datetime.datetime.now()
if str(sid) not in managers:
# server_data was already access-checked above;
# it cannot be None at this point.
manager = ServerManager(server_data)
manager.pga_user = self._current_pga_user()
# Suppress passexec for non-owners of shared
# servers — it runs commands on the client machine
# and must not inherit the owner's.
Expand Down
16 changes: 16 additions & 0 deletions web/pgadmin/utils/driver/psycopg3/server_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def __init__(self, server):
self.tunnel_object = None
self.tunnel_created = False
self.display_connection_string = ''
# fs_uniquifier of the pgAdmin user this manager was built for;
# set by the driver, see Driver._current_pga_user.
self.pga_user = None

self.update(server)

Expand Down Expand Up @@ -154,6 +157,19 @@ def as_dict(self):
res['ver'] = self.ver
res['sversion'] = self.sversion

# Persisted alongside the connection state so a later restore
# (e.g. after a worker restart) can tell whether this blob still
# belongs to the Server row for this id, or whether the id was
# reused by an unrelated row after the configuration database
# was reset/restored - see Driver._manager_is_stale.
res['host'] = self.host
res['port'] = self.port
res['db'] = self.db
res['user'] = self.user
res['service'] = self.service
res['tunnel_host'] = self.tunnel_host
res['pga_user'] = self.pga_user

self._set_password(res)

if self.use_ssh_tunnel:
Expand Down
Loading
Loading