feat(auth): ServicePrincipal + is_service_account discriminator (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:31:49 +08:00
parent 978b0cf24d
commit ec4769a33f
4 changed files with 69 additions and 2 deletions
@@ -0,0 +1,32 @@
"""API key authentication backend (Stage 1 PR2).
Resolves an ``Authorization: Bearer dfk_...`` token into a
``ServicePrincipal`` + workspace + scopes, so ``AuthMiddleware`` can
stamp the same contextvars a cookie-authenticated human would set
(spec D1: user_id = SA.id).
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ServicePrincipal:
"""Non-human principal backing an API key. Satisfies the
``deerflow.runtime.user_context.CurrentUser`` protocol."""
id: str
is_service_account: bool = True
def parse_scopes(scopes: str) -> list[str]:
"""Parse a comma-separated scope string into a permission list.
``"threads:read, threads:write"`` -> ``["threads:read", "threads:write"]``.
Empty / whitespace-only segments are dropped.
"""
return [s.strip() for s in scopes.split(",") if s.strip()]
+5
View File
@@ -31,6 +31,11 @@ class User(BaseModel):
needs_setup: bool = Field(default=False, description="True for auto-created admin until setup completes")
token_version: int = Field(default=0, description="Incremented on password change to invalidate old JWTs")
# Headless API discriminator (Stage 1 PR2). Always False for human
# users; ServicePrincipal sets it True. Lets downstream code branch
# on principal kind without isinstance gymnastics.
is_service_account: bool = Field(default=False, description="True only for API-key service accounts, never for human users")
# Workspace linkage (Stage 0 PR4)
default_workspace_id: str | None = Field(
default=None,