Attribute API-token sessions to the token owner (effective_user) (#871)

Split 2/4 of the companion bridge (#863 was 1/4). A paired bearer-token caller
runs as the sandboxed 'api' pseudo-user, so its sessions were stranded in a
separate 'api'-owned silo, invisible to the owner's desktop UI.

Add effective_user(): for a bearer token it resolves to the token's real owner
(request.state.api_token_owner); for cookie sessions it is identical to
get_current_user, so the swap is a no-op for browser users. Route session
ownership/attribution in routes/session_routes.py through it.

Tests (tests/test_session_owner_attribution.py):
- cookie/browser users are unchanged
- a bearer token attributes to its owner; with no owner it does NOT escalate
- _verify_session_owner: a bearer token for owner A cannot verify owner B's
  session (404); owner verifies their own; missing -> 404; unauth -> 403
This commit is contained in:
Mahdi Salmanzade
2026-06-02 06:39:01 +04:00
committed by GitHub
parent bc00a9fc7f
commit 54ac4a74fb
3 changed files with 145 additions and 8 deletions
+24
View File
@@ -10,6 +10,30 @@ def get_current_user(request: Request) -> Optional[str]:
return getattr(request.state, 'current_user', None)
def effective_user(request: Request):
"""The real human behind the request, for ownership/attribution.
Cookie sessions resolve to the logged-in username. Bearer ``ody_`` callers
come through as the sandboxed pseudo-user "api" so they can't wander into
cookie/user routes by default, but their token was minted by, and belongs
to, a real owner stamped on ``request.state.api_token_owner``. Routes that
should attribute a token's actions to that owner (sessions, chat history)
call this instead of :func:`get_current_user`, so a paired client sees and
creates the SAME data as the owner's desktop UI rather than a separate
"api"-owned silo.
For cookie sessions this is identical to :func:`get_current_user`, so
swapping a route over is a no-op for browser users. A bearer token with no
owner falls back to :func:`get_current_user` (the "api" pseudo-user), so it
never escalates.
"""
if getattr(request.state, "api_token", False):
owner = getattr(request.state, "api_token_owner", None)
if owner:
return owner
return get_current_user(request)
def _auth_disabled() -> bool:
"""True when the operator has explicitly turned off auth via .env.
Mirrors the AUTH_ENABLED parse in app.py / core/middleware.py so the