hub-core/hub_core/security/config.py

63 lines
2.7 KiB
Python
Raw Normal View History

"""Explicit runtime composition: owner facts remain an injected trust adapter."""
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
import httpx
from hub_core.security.audit import AuditCoreSink
from hub_core.security.boundary import AccessController, FactSource
from hub_core.security.identity import OIDCVerifier, require_https
from hub_core.security.policy import FlexPolicy
@dataclass(frozen=True)
class SecuritySettings:
issuer: str
audience: str
root_subject: str
policy_url: str
policy_caller: str
policy_token_file: Path
policy_keys_file: Path
audit_url: str
audit_token_file: Path
def __post_init__(self):
for url in (self.issuer, self.policy_url, self.audit_url):
require_https(url)
if not self.audience or not self.root_subject:
raise ValueError("explicit audience and immutable root subject required")
if not self.policy_caller.startswith("system:serviceaccount:"):
raise ValueError("explicit policy workload principal required")
for path in (self.policy_token_file, self.policy_keys_file, self.audit_token_file):
if not isinstance(path, Path) or not path.is_absolute():
raise ValueError("absolute credential/trust paths required")
if self.policy_token_file == self.audit_token_file:
raise ValueError("policy and audit require separate credentials")
@classmethod
def from_env(cls) -> SecuritySettings | None:
fields = tuple(cls.__dataclass_fields__)
values = {field: os.getenv("HUB_CORE_SECURITY_" + field.upper(), "") for field in fields}
if not any(values.values()):
return None
missing = [field for field, value in values.items() if not value]
if missing:
raise ValueError("incomplete Hub security configuration: " + ", ".join(missing))
return cls(**{field: Path(value) if field.endswith("_file") else value
for field, value in values.items()})
def compose(self, *, facts: FactSource, client: httpx.AsyncClient) -> AccessController:
return AccessController(
identity=OIDCVerifier(issuer=self.issuer, audience=self.audience, client=client),
facts=facts,
policy=FlexPolicy(base_url=self.policy_url, client=client,
caller_token_file=self.policy_token_file,
trusted_keys_file=self.policy_keys_file, caller=self.policy_caller),
audit=AuditCoreSink(base_url=self.audit_url, token_file=self.audit_token_file, client=client),
root_issuer=self.issuer, root_subject=self.root_subject,
)