diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index cfcb324c4d8..93bcc38a147 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -1685,8 +1685,16 @@ def connect(self, gid, sid, is_qt=False, server=None): password = conn_passwd or server.password else: password = data['password'] if 'password' in data else None - save_password = data['save_password']\ - if 'save_password' in data else False + # The password-prompt dialog doesn't resend the server's + # existing save_password setting, only its own checkbox + # state. If the server is already configured to save its + # password, a freshly entered replacement should still be + # persisted, or the stale saved password is never + # replaced. See issue #10128. + save_password = ( + data['save_password'] if 'save_password' in data + else False + ) or bool(server.save_password) try: # Encrypt the password before saving with user's login @@ -1756,6 +1764,16 @@ def connect(self, gid, sid, is_qt=False, server=None): return internal_server_error(errormsg=str(e)) + # Persist the manager's corrected in-memory password (set by + # the driver on a successful connect) into the Flask session. + # Without this, a fresh worker process handling the next + # request (e.g. opening the Query Tool, in a multi-worker + # deployment) restores the stale pre-fix manager state from + # the session and the new password is lost, re-triggering the + # password prompt indefinitely. See issue #10128. + if password: + manager.update_session() + if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD: try: # Save the encrypted tunnel password. diff --git a/web/pgadmin/tools/sqleditor/__init__.py b/web/pgadmin/tools/sqleditor/__init__.py index 3c61c1edb0c..ec131621ff6 100644 --- a/web/pgadmin/tools/sqleditor/__init__.py +++ b/web/pgadmin/tools/sqleditor/__init__.py @@ -2712,7 +2712,7 @@ def connect_server(sid): # password the user just entered at that prompt is cached here so the # tool's connection can use it, instead of being discarded and # re-prompted in a loop. - _cache_manager_password_from_request(manager) + _cache_manager_password_from_request(manager, server) return make_json_response( success=1, info=gettext("Server connected."), @@ -2725,7 +2725,7 @@ def connect_server(sid): ) -def _cache_manager_password_from_request(manager): +def _cache_manager_password_from_request(manager, server=None): """ Cache the password supplied with the current request (from a tool's password prompt) onto the server manager, so that connections opened by @@ -2736,6 +2736,13 @@ def _cache_manager_password_from_request(manager): password, so a freshly entered credential (e.g. a regenerated, short-lived cloud auth token) takes effect immediately. + When "Save Password" is requested and allowed, the freshly entered + password is also persisted to the server record (overwriting any stale + stored ciphertext). Without this, a rotated/regenerated password entered + at the prompt would work for the current session only and the tool would + keep re-using the stale saved password and re-prompt on the next + connection. + This is best-effort: any failure (including malformed request data) is logged and swallowed so it never turns the caller's "Server connected" response into a 500 error. @@ -2756,12 +2763,40 @@ def _cache_manager_password_from_request(manager): if not crypt_key_present: return - manager._update_password(encrypt(password, crypt_key)) + enc_password = encrypt(password, crypt_key) + manager._update_password(enc_password) manager.update_session() + + # Persist the freshly entered password if the user asked to save it, + # so the stale stored ciphertext is replaced. + save_password = data.get('save_password', False) + if save_password in ('true', 'True', '1', 1, True) and \ + ALLOW_SAVE_PASSWORD and server is not None: + _persist_saved_password(server, enc_password) except Exception as e: current_app.logger.exception(e) +def _persist_saved_password(server, enc_password): + """ + Persist the encrypted password to the server record (owned or shared), + replacing any stale stored ciphertext. + """ + from pgadmin.model import db + from pgadmin.browser.server_groups.servers import ServerModule + + target = server + if server.shared and server.user_id != current_user.id: + shared_server = ServerModule.get_shared_server( + server, server.servergroup_id) + if shared_server is not None: + target = shared_server + + setattr(target, 'save_password', 1) + setattr(target, 'password', enc_password) + db.session.commit() + + @blueprint.route( '/filter_dialog/', methods=["PUT"], endpoint='set_filter_data'