From 4e51c9138e83fd4e37199ba9a31daadf966460d8 Mon Sep 17 00:00:00 2001 From: Paulo Date: Mon, 20 Jul 2026 22:11:41 +0200 Subject: [PATCH 1/2] Don't destroy the session when the account lookup returns None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_session_account dropped the session whenever Account.get(account_id) returned None — added in ENG-703 as cleanup for a "deleted account". But nothing deletes accounts (no code path, and the account FKs are ON DELETE RESTRICT), so that case can't happen legitimately. The guard only ever fires on an anomaly — a transient/ambiguous lookup miss — and in exactly that case destroying the session turns a momentary blip into a forced re-login. Remove the drop. A None still 401s the request; the session survives so a transient miss self-heals on the next request. Regression test asserts a missing-account lookup leaves the session key intact. --- backend/druks/accounts/dependencies.py | 12 +++++------- backend/tests/test_auth.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/druks/accounts/dependencies.py b/backend/druks/accounts/dependencies.py index 4532029..8ed0c52 100644 --- a/backend/druks/accounts/dependencies.py +++ b/backend/druks/accounts/dependencies.py @@ -8,19 +8,17 @@ async def resolve_session_account(request: Request) -> Account | None: - """The session cookie's account, or None; drops a session whose account - is gone.""" + """The account behind the session cookie, or None.""" token = request.cookies.get(sessions.SESSION_COOKIE) if not token: return account_id = await sessions.resolve_session(token) if not account_id: return - account = Account.get(account_id) - if not account: - await sessions.drop_session(token) - return - return account + # Never drop the session on a missing account: nothing deletes accounts + # (the FKs forbid it), so a None here is a transient read, not a real + # deletion — dropping would turn a blip into a forced re-login. + return Account.get(account_id) async def current_session_account(request: Request) -> Account: diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index 5892876..82a8233 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -7,6 +7,7 @@ import pytest from conftest import configure_app_for_test, connect_harness, make_settings from druks import database +from druks.accounts.constants import SESSION_PREFIX from druks.accounts.models import Account from druks.accounts.sessions import SESSION_COOKIE from druks.harnesses import base as hbase @@ -128,6 +129,18 @@ def test_redis_eviction_signs_out_but_keeps_credentials(tmp_path, monkeypatch, d ) +def test_missing_account_does_not_drop_the_session(tmp_path, monkeypatch, db_session): + # A None from Account.get is a transient/anomalous read (nothing deletes + # accounts), so it 401s the request but must never destroy the session — + # else a blip becomes a forced re-login. + with _client(tmp_path) as client: + _login(client, monkeypatch) + key = f"{SESSION_PREFIX}{client.cookies[SESSION_COOKIE]}" + druks.redis.get_client()._data[key] = b"no-such-account" + assert client.get("/api/auth/session").status_code == 401 + assert key in druks.redis.get_client()._data + + def test_session_keeps_its_account_across_reconnects(tmp_path, monkeypatch, db_session): with _client(tmp_path) as client: _login(client, monkeypatch, email="me@example.com") From a280a1800582fe85068e737e5e992ea7b2f7ad6f Mon Sep 17 00:00:00 2001 From: Paulo Date: Tue, 21 Jul 2026 08:49:50 +0200 Subject: [PATCH 2/2] Drop the comment and docstring on resolve_session_account --- backend/druks/accounts/dependencies.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/druks/accounts/dependencies.py b/backend/druks/accounts/dependencies.py index 8ed0c52..d00bb70 100644 --- a/backend/druks/accounts/dependencies.py +++ b/backend/druks/accounts/dependencies.py @@ -8,16 +8,12 @@ async def resolve_session_account(request: Request) -> Account | None: - """The account behind the session cookie, or None.""" token = request.cookies.get(sessions.SESSION_COOKIE) if not token: return account_id = await sessions.resolve_session(token) if not account_id: return - # Never drop the session on a missing account: nothing deletes accounts - # (the FKs forbid it), so a None here is a transient read, not a real - # deletion — dropping would turn a blip into a forced re-login. return Account.get(account_id)