feat(frontends,security,api,ui): a frontend can hold a token scope of its own

This commit is contained in:
hh committed 2026-09-06 23:26:26 +02:00
1 parent 60782d6276
commit e79e9fc149
11 files changed
+245 -21

No files matched your search

+16 -7
View File
@@ -21,7 +21,7 @@ from argon2.exceptions import InvalidHashError, VerifyMismatchError
from beaver_gateway.storage import list_active_tokens, touch_token
if TYPE_CHECKING:
from collections.abc import Mapping
from collections.abc import Iterable, Mapping
from beaver_gateway.storage import Database
@@ -35,8 +35,8 @@ class TokenStoreError(ValueError):
"""Malformed ``BOOTSTRAP_TOKENS`` value or duplicate token."""
VALID_SCOPES: frozenset[str] = frozenset({"*", "messages", "mcp", "admin", "api"})
"""The scopes a ``Token.scope`` may hold.
BUILTIN_SCOPES: frozenset[str] = frozenset({"*", "messages", "mcp", "admin", "api"})
"""The scopes the gateway's own frontends define.
* ``*`` — wildcard, may use any frontend
* ``messages`` — Anthropic Messages frontend only
@@ -45,16 +45,24 @@ VALID_SCOPES: frozenset[str] = frozenset({"*", "messages", "mcp", "admin", "api"
* ``admin`` — reserved for programmatic admin access; the AdminFrontend
itself authenticates via session cookies, not bearer tokens, so this
scope is unused today.
A setup's own frontend adds its scope on top; see :func:`scopes_with` and
``GatewayRuntime.scopes``, which is what ``POST /api/tokens`` validates.
"""
def scopes_with(extra: Iterable[str | None]) -> frozenset[str]:
"""The builtin scopes plus every frontend's own; ``None`` entries drop out."""
return BUILTIN_SCOPES | {s for s in extra if s}
@dataclass(frozen=True, slots=True)
class TokenIdentity:
"""What :meth:`TokenStore.verify` resolves to on success.
``token_id`` is the DB row id for persisted tokens, or ``None`` for
an env-bootstrap match (those have no DB row to touch). ``scope``
gates which frontend the token may hit (see :data:`VALID_SCOPES`);
gates which frontend the token may hit (see :data:`BUILTIN_SCOPES`);
bootstrap tokens implicitly get ``"*"``.
"""
@@ -67,8 +75,8 @@ class TokenIdentity:
``"*"`` is the wildcard; an exact match satisfies a single scope.
Unknown ``required`` values intentionally fall through to a
strict equality check — callers should pass one of
:data:`VALID_SCOPES`.
strict equality check — callers should pass a scope the runtime
knows (:data:`BUILTIN_SCOPES` plus every frontend's own).
"""
return self.scope in ("*", required)
@@ -325,11 +333,12 @@ class TokenStore:
__all__ = [
"VALID_SCOPES",
"BUILTIN_SCOPES",
"TokenIdentity",
"TokenStore",
"TokenStoreError",
"hash_token",
"scopes_with",
]