From 4f9116e3fef148def4acded6ad2063b2cfb13eba Mon Sep 17 00:00:00 2001 From: 1445043649 <> Date: Sun, 28 Jun 2026 11:37:06 +0800 Subject: [PATCH] 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) --- .../harness/deerflow/runtime/user_context.py | 15 ++++++++------- backend/tests/test_api_key_backend.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/packages/harness/deerflow/runtime/user_context.py b/backend/packages/harness/deerflow/runtime/user_context.py index 02e586a4..85b836b3 100644 --- a/backend/packages/harness/deerflow/runtime/user_context.py +++ b/backend/packages/harness/deerflow/runtime/user_context.py @@ -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) diff --git a/backend/tests/test_api_key_backend.py b/backend/tests/test_api_key_backend.py index d378e18f..573ab640 100644 --- a/backend/tests/test_api_key_backend.py +++ b/backend/tests/test_api_key_backend.py @@ -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]