Stdlib per-tenant service: trusted-header identity (router/relay/worker, constant-time keys, slot pinned to the pod), fail-closed card flags with capability negotiation, tenant-scoped store (atomic writes, revisions, append-only ledgers, content-addressed blobs, manifest), audit outcome for every request, and the /hux/v1 pipeline that maps errors to hux.error.v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNPhwu2bsaRNg3DETSAZoM
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""Caller identity resolved from the trusted hop's headers.
|
|
|
|
The chat router, the Telegram relay and the Worker are the only callers. Each
|
|
asserts identity in headers; the request body is never trusted for identity.
|
|
Relay and worker callers must also present their shared key, compared in
|
|
constant time against the value the pod was started with.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hmac
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass
|
|
from collections.abc import Mapping
|
|
|
|
from hux.errors import Unauthorized
|
|
|
|
HEADER_SLOT = "X-Hermes-Tenant-Identity"
|
|
HEADER_SUBJECT = "X-Hux-Subject"
|
|
HEADER_SURFACE = "X-Hux-Surface"
|
|
HEADER_TRUST = "X-Hux-Trust"
|
|
HEADER_KEY = "X-Hux-Relay-Key"
|
|
|
|
SLOT_RE = re.compile(r"^slot-[0-9]{1,3}$")
|
|
SUBJECT_RE = re.compile(r"^usr_[0-9a-f]{16,64}$")
|
|
SURFACES = ("chat", "worker", "telegram", "voice", "api")
|
|
TRUSTS = ("router", "relay", "worker")
|
|
KEY_ENV = {"relay": "HUX_RELAY_KEY", "worker": "HUX_WORKER_KEY"}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Identity:
|
|
"""Who is calling, on which surface, asserted by which trusted hop."""
|
|
|
|
tenant_slot: str
|
|
subject: str
|
|
surface: str
|
|
trust: str
|
|
|
|
def record(self) -> dict[str, str]:
|
|
"""Serialise as ``common.identity``."""
|
|
return {"tenant_slot": self.tenant_slot, "subject": self.subject, "surface": self.surface, "trust": self.trust}
|
|
|
|
|
|
def _header(headers: Mapping[str, str], name: str) -> str:
|
|
for key, value in headers.items():
|
|
if key.lower() == name.lower():
|
|
return value.strip()
|
|
return ""
|
|
|
|
|
|
def resolve(headers: Mapping[str, str], environ: Mapping[str, str] | None = None) -> Identity:
|
|
"""Build an Identity from request headers or raise Unauthorized.
|
|
|
|
``trust`` defaults to ``router``; relay and worker trust require the
|
|
matching shared key to be configured and presented.
|
|
"""
|
|
environ = os.environ if environ is None else environ
|
|
slot = _header(headers, HEADER_SLOT)
|
|
subject = _header(headers, HEADER_SUBJECT)
|
|
surface = _header(headers, HEADER_SURFACE) or "chat"
|
|
trust = _header(headers, HEADER_TRUST) or "router"
|
|
if not SLOT_RE.match(slot):
|
|
raise Unauthorized("missing or malformed tenant slot")
|
|
own_slot = environ.get("HUX_TENANT_SLOT", "")
|
|
if own_slot and slot != own_slot:
|
|
raise Unauthorized("tenant slot does not belong to this pod")
|
|
if not SUBJECT_RE.match(subject):
|
|
raise Unauthorized("missing or malformed subject")
|
|
if surface not in SURFACES or trust not in TRUSTS:
|
|
raise Unauthorized("unknown surface or trust")
|
|
if trust in KEY_ENV:
|
|
expected = environ.get(KEY_ENV[trust], "")
|
|
presented = _header(headers, HEADER_KEY)
|
|
if not expected or not presented or not hmac.compare_digest(expected, presented):
|
|
raise Unauthorized(f"{trust} key missing or wrong")
|
|
if trust == "router" and surface == "worker":
|
|
raise Unauthorized("worker surface needs worker trust")
|
|
return Identity(slot, subject, surface, trust)
|