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:
@@ -42,17 +42,18 @@ from typing import Final, Protocol, runtime_checkable
|
|||||||
class CurrentUser(Protocol):
|
class CurrentUser(Protocol):
|
||||||
"""Structural type for the current authenticated user.
|
"""Structural type for the current authenticated user.
|
||||||
|
|
||||||
Requires ``.id: str`` plus ``.is_service_account: bool`` — the latter
|
Requires only ``.id: str`` — the persistence layer reads nothing else,
|
||||||
distinguishes a human (cookie/JWT) principal from a headless service
|
and keeping the contract minimal lets any ``.id``-bearing object (incl.
|
||||||
account (API key). Concrete implementations:
|
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.models.User`` (False) and
|
||||||
``app.gateway.auth.api_key_backend.ServicePrincipal`` (True).
|
``app.gateway.auth.api_key_backend.ServicePrincipal`` (True). Since that
|
||||||
Readers that may run before either is set should use
|
attribute is NOT part of this structural contract, app-layer readers
|
||||||
``getattr(user, "is_service_account", False)``.
|
must access it defensively: ``getattr(user, "is_service_account", False)``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
is_service_account: bool
|
|
||||||
|
|
||||||
|
|
||||||
_current_user: Final[ContextVar[CurrentUser | None]] = ContextVar("deerflow_current_user", default=None)
|
_current_user: Final[ContextVar[CurrentUser | None]] = ContextVar("deerflow_current_user", default=None)
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.gateway.auth.api_key_backend import ServicePrincipal, parse_scopes
|
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")
|
p = ServicePrincipal(id="sa-1")
|
||||||
assert p.id == "sa-1"
|
assert p.id == "sa-1"
|
||||||
assert p.is_service_account is True
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user