2026-07-22 21:21:05 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-22 21:48:09 +02:00
|
|
|
from collections.abc import Mapping
|
|
|
|
|
|
2026-07-22 21:21:05 +02:00
|
|
|
from fastapi import Request
|
|
|
|
|
|
|
|
|
|
from qonto_assistant.config import Settings
|
|
|
|
|
from qonto_assistant.contracts import ActorClaims
|
2026-07-24 00:10:41 +02:00
|
|
|
from qonto_assistant.key_cape_auth import KeyCapeAuthError, KeyCapeTokenVerifier
|
2026-07-22 21:21:05 +02:00
|
|
|
|
|
|
|
|
|
2026-07-24 00:10:41 +02:00
|
|
|
def actor_claims_from_headers(
|
|
|
|
|
headers: Mapping[str, str],
|
|
|
|
|
settings: Settings,
|
|
|
|
|
*,
|
|
|
|
|
key_cape_verifier: KeyCapeTokenVerifier | None = None,
|
|
|
|
|
) -> ActorClaims:
|
2026-07-22 21:48:09 +02:00
|
|
|
"""Shared REST/MCP claims parsing so both transports enforce identical actor identity.
|
|
|
|
|
|
2026-07-24 00:10:41 +02:00
|
|
|
When a `KeyCapeTokenVerifier` is configured (QONTO-WP-0004-T03), a
|
|
|
|
|
verified `Authorization: Bearer <jwt>` takes precedence over the
|
|
|
|
|
self-asserted `X-Actor-*` header convention -- closing the gap
|
|
|
|
|
`docs/mcp-integration.md` called out explicitly. If the verifier is
|
|
|
|
|
marked `required`, a missing or invalid bearer token is rejected
|
|
|
|
|
outright rather than silently falling back to self-asserted headers.
|
2026-07-22 21:48:09 +02:00
|
|
|
"""
|
2026-07-24 00:10:41 +02:00
|
|
|
authorization = headers.get("authorization", "")
|
|
|
|
|
if authorization.lower().startswith("bearer ") and key_cape_verifier is not None:
|
|
|
|
|
token = authorization.split(" ", 1)[1].strip()
|
|
|
|
|
return key_cape_verifier.verify(token)
|
|
|
|
|
|
|
|
|
|
if key_cape_verifier is not None and key_cape_verifier.required:
|
|
|
|
|
raise KeyCapeAuthError("bearer_token_required")
|
|
|
|
|
|
2026-07-22 21:48:09 +02:00
|
|
|
actor_id = headers.get("x-actor-id", "anonymous")
|
|
|
|
|
tenant_id = headers.get("x-tenant-id", settings.default_tenant_id)
|
|
|
|
|
lane = headers.get("x-actor-lane", settings.default_actor_lane)
|
|
|
|
|
raw_scopes = headers.get("x-actor-scopes", "")
|
2026-07-22 21:21:05 +02:00
|
|
|
scopes = frozenset(scope.strip() for scope in raw_scopes.split(",") if scope.strip())
|
|
|
|
|
return ActorClaims(actor_id=actor_id, tenant_id=tenant_id, lane=lane, scopes=scopes)
|
2026-07-22 21:48:09 +02:00
|
|
|
|
|
|
|
|
|
2026-07-24 00:10:41 +02:00
|
|
|
def actor_claims_from_request(
|
|
|
|
|
request: Request,
|
|
|
|
|
settings: Settings,
|
|
|
|
|
*,
|
|
|
|
|
key_cape_verifier: KeyCapeTokenVerifier | None = None,
|
|
|
|
|
) -> ActorClaims:
|
|
|
|
|
return actor_claims_from_headers(request.headers, settings, key_cape_verifier=key_cape_verifier)
|