"""Join admitted owner observations; this module defines no owner HTTP API. Reader implementations must authenticate their own workload and return current, side-effect-free observations. A mapping reference is owner review evidence, not an entitlement grant. Neither token roles nor static root allowlists suffice. """ from __future__ import annotations import asyncio import hashlib import json import math import time from dataclasses import dataclass from typing import Protocol from hub_core.security.boundary import LiveFacts from hub_core.security.identity import AccessFailure, Actor @dataclass(frozen=True) class AccountObservation: issuer: str subject: str tenant: str account_active: bool root_entitled: bool checked_at: float evidence_id: str producer_addresses: frozenset[str] = frozenset() @dataclass(frozen=True) class TenantObservation: identifier: str active: bool checked_at: float evidence_id: str class AccountReader(Protocol): async def read(self, *, issuer: str, subject: str, tenant: str) -> AccountObservation: """Authenticated read; unknown identities must not be provisioned.""" ... class TenantReader(Protocol): async def read(self, *, identifier: str) -> TenantObservation: """Authenticated current lifecycle lookup with owner evidence.""" ... class PlatformFacts: """Root-first, platform-only composition with no authority cache. account_tenant and mapping_evidence require explicit owner-reviewed mapping to tenant:platform. They carry no default alias or implicit role translation. Workload and cross-tenant admission need a separately reviewed contract. """ def __init__(self, *, accounts: AccountReader, tenants: TenantReader, account_tenant: str, mapping_evidence: str): _reference(account_tenant) _reference(mapping_evidence) self.accounts, self.tenants = accounts, tenants self.account_tenant, self.mapping_evidence = account_tenant, mapping_evidence async def resolve(self, actor: Actor, resource: str) -> LiveFacts: if actor.principal_type != 'human' or actor.tenant != 'tenant:platform': raise AccessFailure(403, 'unsupported_facts_principal') try: # A single budget includes both observations. Sequential reads avoid # orphaned work on failure; preserve the oldest source timestamp. async with asyncio.timeout(3): account = await self.accounts.read(issuer=actor.issuer,subject=actor.subject, tenant=self.account_tenant) tenant = await self.tenants.read(identifier='tenant:platform') if not isinstance(account,AccountObservation) or not isinstance(tenant,TenantObservation): raise ValueError('typed owner observations required') if (account.issuer,account.subject,account.tenant) != ( actor.issuer,actor.subject,self.account_tenant): raise ValueError('account binding mismatch') if tenant.identifier != 'tenant:platform': raise ValueError('tenant binding mismatch') for value in (account.account_active,account.root_entitled,tenant.active): if type(value) is not bool: raise ValueError('explicit owner state required') now = time.time() for observed in (account,tenant): _reference(observed.evidence_id) if (type(observed.checked_at) not in {int,float} or not math.isfinite(observed.checked_at) or not 0 <= now-observed.checked_at <= 5): raise ValueError('stale owner observation') if not isinstance(account.producer_addresses,frozenset): raise ValueError('immutable producer bindings required') for address in account.producer_addresses: _reference(address) evidence = 'sha256:' + hashlib.sha256(json.dumps({ 'account':account.evidence_id,'tenant':tenant.evidence_id, 'mapping':self.mapping_evidence, },sort_keys=True,separators=(',',':')).encode()).hexdigest() return LiveFacts(actor.issuer,actor.subject,actor.tenant,'tenant:platform', account.account_active,tenant.active,tenant.active, account.root_entitled,min(account.checked_at,tenant.checked_at), evidence,account.producer_addresses) except Exception as exc: raise AccessFailure(503,'owner_facts_unavailable_or_untrusted') from exc def _reference(value): if not isinstance(value,str) or not value.strip(): raise ValueError('nonempty owner reference required')