Skip to content

users: tenant changes are invisible until re login because UserContext is cached in the session #362

Description

@antosubash

Summary

UsersAuthProvider.resolve_user caches the full UserContext (including tenant_id) inside the signed session cookie and returns it on every subsequent request without re-checking the DB, as long as the cached id matches. Changing a logged-in user's tenant_id in the database (the only way to assign one at all today — see the companion UserCreate/UserUpdate issue) has no effect on that user's active session: they keep acting as their old tenant until they log out and back in.

Observed

users/provider.py:40-43 (UsersAuthProvider.resolve_user):

cached = UserContext.from_session_dict(session.get(_SESSION_USER_CTX_KEY))
if cached is not None and cached.id == user_id_str:
    return cached

No version/timestamp check against the DB row — a cache hit on id alone short-circuits the DB lookup entirely. UserContext.tenant_id is part of what's cached (auth/contracts/schemas.py, to_session_dict/from_session_dict, and from_user's tenant_id=user.tenant_id).

Minimal reproduction (no DB, demonstrates the cache contract directly):

from auth.contracts.schemas import UserContext

# What gets written into the session cookie right after login, tenant A:
ctx = UserContext(id="u1", email="a@example.com", name="A", roles=["admin"], tenant_id="A")
cached_payload = ctx.to_session_dict()

# ... an operator now runs `UPDATE users_user SET tenant_id='B' WHERE id='u1'` ...

# provider.py:40-43's fast path: cached.id == user_id_str is still true, so this is
# returned as-is with NO query against the row that was just changed.
still_cached = UserContext.from_session_dict(cached_payload)
assert still_cached.tenant_id == "A"   # stale — the DB now says 'B'
print("session still carries the old tenant until logout/login")

Confirmed on the real stack in the spike (FACT 3″, test_spike_app_tenancy.py::test_3_multi_tenant_host_resolution): after UPDATE users_user SET tenant_id='A' WHERE id=<admin> on the same browser session, /whoami kept reporting the pre-update state; only a fresh session cookie picked up the new tenant.

Expected

A tenant_id (or role) change on a user should take effect on their next request, not wait for them to log out and back in.

Why a module cannot work around it

The session cache and its invalidation live entirely in users/auth; records (or any consuming module) has no hook into UsersAuthProvider.resolve_user's cache-hit path to force a re-check.

Proposed API

A version stamp on the user row (bumped whenever tenant_id or role membership changes) included in the cached payload; resolve_user's fast path compares it against the current DB value (a cheap indexed lookup) before trusting the cache, instead of trusting id alone.

Context

Found while making the records module (antosubash/smpy_modules#37) multi-tenant; design doc docs/plans/2026-09-23-records-multitenancy.md there.

Activity

  1. antosubash commented on Oct 1, 2026

    @antosubash
    OwnerAuthor

    Closing: filed before #370 (fail-closed tenancy + tenants module) landed, so the premise is stale. Module tenancy adoption is being re-tracked with fresh per-module issues in the post-#370 format (see #371–#377); any remaining gap here gets a new issue and lands in the tenancy adoption PR stack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions