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):
|
||||
"""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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user