"""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)