"""Owner-derived metadata pilot compiler. Source-only; never authorizes or dispatches. OwnerConfig is installed by the trusted owner, never deserialized from a request. Six fixed calls on two structure objects. Recompile immediately before each call. Canonical PDP request digest is opaque: the authenticated mediator verifies it. """ from __future__ import annotations import builtins import hashlib import json import os from pathlib import Path import re import stat import sys import types from dataclasses import dataclass from uuid import UUID, uuid4 import yaml class Refused(Exception): pass def need(value, reason): if not value: raise Refused(reason) def encoded(value): return json.dumps(value,sort_keys=True,separators=(',', ':'),ensure_ascii=True,allow_nan=False).encode() def content_hash(raw): return hashlib.sha256(raw).hexdigest() def tagged(raw): return 'sha256:'+content_hash(raw) def digest_ok(value): return type(value) is str and re.fullmatch(r'sha256:[0-9a-f]{64}',value) is not None class UniqueLoader(yaml.SafeLoader): pass def unique(loader,node): result={} for key,val in node.value: k=loader.construct_object(key) need(k not in result,'duplicate_owner_key') result[k]=loader.construct_object(val) return result UniqueLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,unique) TARGET_KEYS=('kind','org','repo','mount','path','fields','mount_management','consumers', 'delivery_modes','delivery_auth','delivery_config','auth_capability','workload_delivery') REQUEST_KEYS={'action','kind','catalog_id','operation_id','request_digest','ccr_sha256','catalog_sha256','operations'} @dataclass(frozen=True) class OwnerConfig: platform_repo: Path secrets_repo: Path ccr_filename: str catalog_filename: str catalog_directory: Path # Exact path/hash pairs, configured independently by the owner. No request paths. source_pins: tuple[tuple[str,str], ...] @dataclass(frozen=True) class Plan: serialized: bytes @property def content_digest(self): return content_hash(self.serialized) # Journal uses bare content hash. def metadata(self): try: return json.loads(self.serialized) except Exception: raise Refused('invalid_plan') from None @dataclass(frozen=True) class Call: ordinal: int kind: str method: str path: str payload: bytes @property def payload_sha256(self): return tagged(self.payload) @property def call_digest(self): return content_hash(encoded({'ordinal':self.ordinal,'kind':self.kind,'method':self.method, 'path':self.path,'payload_sha256':self.payload_sha256})) def cleanup_specs(operation_id): """Deterministic owner cleanup plan; native handles stay in owner memory.""" try: need(str(UUID(operation_id)) == operation_id, 'operation_id_invalid') except (ValueError, AttributeError, TypeError): raise Refused('operation_id_invalid') from None policy_name = 'task-exec-' + operation_id specs = [] for kind, slot, calls in [ ('task-token-revoke', 'task', [('POST','auth/token/revoke-self'), ('POST','auth/token/lookup-accessor')]), ('identity-token-revoke', 'identity', [('POST','auth/token/revoke-self'), ('POST','auth/token/lookup-accessor')]), ('grant-policy-delete', 'owner', [('DELETE','sys/policies/acl/'+policy_name), ('GET','sys/policies/acl/'+policy_name)]), ]: value = {'kind':kind, 'credential_slot':slot, 'operation_id':operation_id, 'calls':[{'method':method,'path':path} for method,path in calls]} if kind == 'task-token-revoke': value['confirm_not_created_if_no_issue_attempt'] = True if kind == 'grant-policy-delete': value['skip_delete_if_no_create_attempt'] = True value['call_digest'] = content_hash(encoded(value)) specs.append(value) return specs class Compiler: def __init__(self, config: OwnerConfig): need(type(config) is OwnerConfig,'owner_configuration_required') self.config=config for root in (config.platform_repo,config.secrets_repo,config.catalog_directory): need(type(root) is Path or isinstance(root,Path),'owner_root_invalid') need(root.is_absolute() and root.resolve()==root,'owner_root_invalid') for name in (config.ccr_filename,config.catalog_filename): need(type(name) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*\.yaml',name),'owner_filename_invalid') self.ccr=config.platform_repo/'credential-change-requests'/config.ccr_filename self.catalog=config.catalog_directory/config.catalog_filename self.validator=config.platform_repo/'scripts/credential-change.py' self.catalog_code=config.secrets_repo/'src/secrets_engine/catalog.py' self.dependencies=[config.secrets_repo/'src/secrets_engine'/name for name in ('__init__.py','errors.py','redact.py')] pins=dict(config.source_pins) self.pins=types.MappingProxyType(pins) need(len(self.pins)==len(config.source_pins) and all(type(k) is str and digest_ok(v) for k,v in self.pins.items()),'owner_pins_invalid') # Compiler source is part of the owner plan, not just generated body hashes. required={str(p) for p in [Path(__file__).resolve(),self.ccr,self.catalog,self.validator,self.catalog_code,*self.dependencies]} need(required <= set(self.pins),'owner_source_pin_missing') extra=set(self.pins)-required need(len(extra)==1,'closed_owner_source_set_required') artifact=Path(next(iter(extra))) need(artifact.parent==config.platform_repo/'openbao/policies' and re.fullmatch(r'workload-kv-read-[a-z0-9-]+\.hcl',artifact.name), 'closed_owner_policy_source_required') def read(self,path): need(str(path) in self.pins and path.is_absolute() and path.resolve()==path,'owner_source_path_invalid') fd=None try: fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) before=os.fstat(fd) need(stat.S_ISREG(before.st_mode) and before.st_nlink==1 and not before.st_mode & 0o022 and 0