fix(auth): keep CurrentUser protocol id-only; is_service_account is opt-in (Stage 1 PR2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-06-28 11:37:06 +08:00
parent ec4769a33f
commit 4f9116e3fe
2 changed files with 18 additions and 7 deletions
@@ -42,17 +42,18 @@ from typing import Final, Protocol, runtime_checkable
class CurrentUser(Protocol):
"""Structural type for the current authenticated user.
Requires ``.id: str`` plus ``.is_service_account: bool`` — the latter
distinguishes a human (cookie/JWT) principal from a headless service
account (API key). Concrete implementations:
Requires only ``.id: str`` — the persistence layer reads nothing else,
and keeping the contract minimal lets any ``.id``-bearing object (incl.
test fixtures) satisfy it. A principal MAY additionally carry
``.is_service_account: bool`` to distinguish a headless service account
(API key) from a human; concrete carriers are
``app.gateway.auth.models.User`` (False) and
``app.gateway.auth.api_key_backend.ServicePrincipal`` (True).
Readers that may run before either is set should use
``getattr(user, "is_service_account", False)``.
``app.gateway.auth.api_key_backend.ServicePrincipal`` (True). Since that
attribute is NOT part of this structural contract, app-layer readers
must access it defensively: ``getattr(user, "is_service_account", False)``.
"""
id: str
is_service_account: bool
_current_user: Final[ContextVar[CurrentUser | None]] = ContextVar("deerflow_current_user", default=None)
+10
View File
@@ -2,6 +2,10 @@
from __future__ import annotations
import dataclasses
import pytest
from app.gateway.auth.api_key_backend import ServicePrincipal, parse_scopes
@@ -22,3 +26,9 @@ def test_service_principal_is_service_account_true_by_default():
p = ServicePrincipal(id="sa-1")
assert p.id == "sa-1"
assert p.is_service_account is True
def test_service_principal_is_frozen():
p = ServicePrincipal(id="sa-1")
with pytest.raises(dataclasses.FrozenInstanceError):
p.id = "other" # type: ignore[misc]