railiance-platform/scripts/task_execution_plan.py
codex d042d468df
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
feat(execution): bind immutable task grants before native effects
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
2026-10-16 07:51:42 +02:00

353 lines
21 KiB
Python

"""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], ...]
grant_filename: str | None = None
@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')]
self.grant_record=None;self.grant_validator=None
if config.grant_filename is not None:
need(type(config.grant_filename) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*\.yaml',config.grant_filename)
and config.grant_filename!=config.ccr_filename,'owner_grant_filename_invalid')
self.grant_record=config.platform_repo/'credential-change-requests'/config.grant_filename
self.grant_validator=Path(__file__).resolve().parent/'task_execution_grant.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]}
if self.grant_record is not None:required.update({str(self.grant_record),str(self.grant_validator)})
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<before.st_size<=1024*1024,'owner_source_file_invalid')
pieces=[];size=0
while size<=1024*1024:
chunk=os.read(fd,min(4096,1024*1024+1-size))
if not chunk: break
size+=len(chunk);pieces.append(chunk)
after=os.fstat(fd);raw=b''.join(pieces)
identity=lambda x:(x.st_dev,x.st_ino,x.st_size,x.st_mtime_ns,x.st_ctime_ns,x.st_mode,x.st_nlink)
need(identity(before)==identity(after) and size==before.st_size and tagged(raw)==self.pins[str(path)],'owner_source_changed')
return raw
except (OSError,ValueError): raise Refused('owner_source_unavailable') from None
finally:
if fd is not None: os.close(fd)
def _catalogue_entry(self, snapshot):
# Execute the verified bytes, not cached imports or timestamp-valid .pyc
# files. Private names support dataclass annotation lookup without replacing
# the application's secrets_engine modules, even during concurrent compiles.
prefix = '_platform_catalogue_' + uuid4().hex
modules = {}
registered = []
def snapshot_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == 'secrets_engine' or name.startswith('secrets_engine.'):
need(level == 0 and name in modules, 'unpinned_catalogue_import')
module = modules[name]
if fromlist:
need(all(hasattr(module, item) for item in fromlist), 'unpinned_catalogue_import')
return module
return modules['secrets_engine']
need(level == 0, 'unpinned_catalogue_import')
return builtins.__import__(name, globals, locals, fromlist, level)
try:
sources = [('secrets_engine', self.dependencies[0]),
('secrets_engine.errors', self.dependencies[1]),
('secrets_engine.redact', self.dependencies[2]),
('secrets_engine.catalog', self.catalog_code)]
for name, path in sources:
private_name = prefix + name[len('secrets_engine'):]
module = types.ModuleType(private_name)
module.__file__ = str(path)
module.__package__ = prefix
module.__dict__['__builtins__'] = dict(vars(builtins), __import__=snapshot_import)
modules[name] = module
sys.modules[private_name] = module
registered.append(private_name)
if name != 'secrets_engine':
setattr(modules['secrets_engine'], name.rsplit('.', 1)[1], module)
exec(compile(snapshot[str(path)], str(path), 'exec'), module.__dict__)
return modules['secrets_engine.catalog'].validate_entry(
yaml.load(snapshot[str(self.catalog)], Loader=UniqueLoader),
source='<owner-pinned-catalogue>')
finally:
for name in registered:
sys.modules.pop(name, None)
def _derive(self):
# No native operations, CLI applier, subprocess, network or ambient credentials.
snapshot={p:self.read(Path(p)) for p in self.pins}
try:
ccr=yaml.load(snapshot[str(self.ccr)],Loader=UniqueLoader)
need(type(ccr) is dict and ccr.get('request_type')=='workload-kv-read','unsupported_ccr_kind')
policy_name=ccr['openbao']['policy_name']
need(type(ccr['openbao']['kv_path']) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.-]*(/[A-Za-z0-9][A-Za-z0-9_.-]*)+',ccr['openbao']['kv_path']),'unsafe_logical_kv_path')
need(type(policy_name) is str and re.fullmatch(r'workload-kv-read-[a-z0-9-]+',policy_name),'unsafe_policy_target')
policy=self.config.platform_repo/'openbao/policies'/f'{policy_name}.hcl'
need(ccr['openbao']['policy_file']==str(policy.relative_to(self.config.platform_repo)),'unsafe_policy_source')
need(str(policy) in snapshot,'owner_policy_pin_missing')
owner=types.ModuleType('owner_credential_change');owner.__file__=str(self.validator)
exec(compile(snapshot[str(self.validator)],str(self.validator),'exec'),owner.__dict__)
# Reuse full existing validator on the pinned parsed bytes; never choose alternate CCR_DIR.
owner.load_yaml=lambda p: ccr if p==self.ccr else (_ for _ in ()).throw(Refused('unexpected_owner_read'))
_ccr,errors,_warnings=owner.validate_ccr(self.ccr)
need(not errors,'owner_ccr_invalid')
need(ccr['status']=='approved','owner_ccr_not_fresh_approved')
need(not owner.applier_readiness_blockers(ccr) and not owner.apply_blockers(ccr),'owner_ccr_not_ready')
need(ccr['openbao']['auth']['method'] in {'oidc','kubernetes'},'unsupported_auth_kind')
generated=owner.generated_policy_hcl(ccr)
need(owner.normalized_policy_body(snapshot[str(policy)].decode())==owner.normalized_policy_body(generated),'owner_policy_not_generated')
# Use the actual consumer catalog validator/defaults, not an alternative broker catalog.
entry=self._catalogue_entry(snapshot)
need(entry.id==ccr['access_frontdoor']['catalog_id'] and entry.stage=='prod'
and ccr['target']['environment']=='production' and entry.kind=='kv'
and entry.mount_management=='existing'
and entry.delivery_auth_management in {'none','existing'},'catalogue_not_metadata_pilot')
need(entry.mount==ccr['openbao']['mount'] and entry.path==ccr['openbao']['kv_path'][len(entry.mount)+1:]
and sorted(entry.fields)==sorted(ccr['openbao']['fields'])
and entry.approval.get('decision_ref')==ccr['id'],'catalogue_ccr_mismatch')
target={k:getattr(entry,k) for k in TARGET_KEYS}
auth=ccr['openbao']['auth'];role_path=f"auth/{auth['mount']}/role/{auth['role']}"
policy_path=f'sys/policies/acl/{policy_name}'
bodies=[b'',encoded({'policy':generated.rstrip()}),b'',b'',encoded(owner.auth_payload(ccr)),b'']
paths=[policy_path,policy_path,policy_path,role_path,role_path,role_path]
methods=['GET','PUT','GET','GET','PUT','GET']
kinds=['policy_baseline','policy_write','policy_readback','role_baseline','auth_role_write','role_readback']
calls=tuple(Call(i,kinds[i],methods[i],paths[i],bodies[i]) for i in range(6))
need(all(re.fullmatch(r'(sys/policies/acl/workload-kv-read-[a-z0-9-]+|auth/(netkingdom|kubernetes)/role/[a-z0-9-]+)',c.path) for c in calls),'unsafe_structure_target')
# Detect source changes during existing validator/generator reads and catalog validation.
for p,raw in snapshot.items(): need(self.read(Path(p))==raw,'owner_source_changed')
return ccr,target,calls
except Refused: raise
except BaseException: raise Refused('owner_compilation_refused') from None
def proposal(self):
ccr,target,calls=self._derive()
return {'action':'execute-ccr','kind':'workload-kv-read','catalog_id':ccr['access_frontdoor']['catalog_id'],
'ccr_sha256':self.pins[str(self.ccr)],'catalog_sha256':tagged(json.dumps(target,sort_keys=True,separators=(',', ':'),ensure_ascii=False,allow_nan=False).encode('utf-8')),
'catalog_target':json.loads(encoded(target)),
'operations':[{'method':c.method,'path':c.path,'payload_sha256':c.payload_sha256} for c in calls]}
def compile(self,request):
need(type(request) is dict and set(request)==REQUEST_KEYS,'closed_plan_request_required')
need(digest_ok(request['request_digest']),'canonical_request_digest_required')
try: need(str(UUID(request['operation_id']))==request['operation_id'],'operation_id_invalid')
except (ValueError,AttributeError,TypeError): raise Refused('operation_id_invalid') from None
expected=self.proposal()
need(all(request[k]==expected[k] for k in REQUEST_KEYS-{'operation_id','request_digest'}),'owner_plan_binding_mismatch')
return Plan(encoded({'schema':'platform-owner-typed-plan/v1','action':'execute-ccr','operation':'metadata-apply-ccr',
'request':request,'catalog_target':expected['catalog_target'],
'source_pins':dict(self.pins),'task_token_max_ttl':120,'task_token_max_uses':12,
'cleanup_uses_reserved':1,'cleanup':cleanup_specs(request['operation_id'])}))
def grant_effects(self,plan,descriptor):
"""Independently derive the one proposed grant's effectful immutable body."""
need(type(plan) is Plan and type(descriptor) is dict,'typed_grant_inputs_required')
meta=plan.metadata();req=meta['request']
need(self.compile(req).serialized==plan.serialized,'immutable_plan_changed')
_,_,calls=self._derive()
budget=descriptor['backend_budget']
need(type(budget) is dict and set(budget)=={'ttl_seconds','uses'}
and type(budget['ttl_seconds']) is int and 30<=budget['ttl_seconds']<=120
and type(budget['uses']) is int and budget['uses']==12,'fixed_grant_budget_required')
operation_id=req['operation_id'];policy_name='task-exec-'+operation_id
paths=sorted({call.path for call in calls})
hcl=''.join('path '+json.dumps(path)+' {\n capabilities = ["read", "create", "update"]\n}\n' for path in paths)
hcl+='path "auth/token/revoke-self" {\n capabilities = ["update"]\n}\n'
token_body={'policies':[policy_name],'no_default_policy':True,'renewable':False,
'ttl':str(budget['ttl_seconds'])+'s','explicit_max_ttl':str(budget['ttl_seconds'])+'s',
'num_uses':12,'type':'service','display_name':policy_name,'no_parent':False}
# A grant cannot include its own immutable hash or raw file SHA. The
# existing descriptor joins it by ID+immutable SHA after this projection.
descriptor_binding={k:v for k,v in descriptor.items() if k!='grant_immutable_sha256'}
pins={path:digest for path,digest in self.pins.items() if self.grant_record is None or path!=str(self.grant_record)}
execution={'action':'execute-ccr','catalog_id':req['catalog_id'],
'descriptor_binding':descriptor_binding,'source_pins':pins,
'cleanup':cleanup_specs(operation_id),'cleanup_uses_reserved':1,
'cleanup_after_business_deadline':True}
openbao={'policy':{'name':policy_name,'hcl':hcl},'token_create_body':token_body,
'owner_parent_custody':'platform','identity_policies':[]}
return execution,openbao
def _bind_grant(self,plan,descriptor):
need(self.grant_record is not None,'owner_grant_source_required')
# Execute exactly the pinned pure projection source, never a cached module.
module=types.ModuleType('_owner_task_execution_grant')
raw=self.read(self.grant_validator)
exec(compile(raw,str(self.grant_validator),'exec'),module.__dict__)
record=yaml.load(self.read(self.grant_record),Loader=UniqueLoader)
execution,openbao=self.grant_effects(plan,descriptor)
try:
projection=module.bind(record,grant_id=descriptor['execution_grant_ccr_id'],
expected_execution=execution,expected_openbao=openbao,
expected_digest=descriptor['grant_immutable_sha256'])
except Exception:raise Refused('owner_grant_binding_mismatch') from None
need(self.read(self.grant_validator)==raw,'owner_grant_source_changed')
return projection
def bind_execution(self, plan, descriptor, owner_descriptor):
"""Bind exact engine descriptor to owner plan; no authorization is created.
owner_descriptor must be installed independently by the owner, never copied
from the request. The engine must separately validate its closed schema and
full canonical request digest/claim/PDP/consume correspondence.
"""
need(type(plan) is Plan and type(descriptor) is dict and type(owner_descriptor) is bytes,
'independent_owner_descriptor_required')
meta=plan.metadata()
need(self.compile(meta['request']).serialized == plan.serialized, 'immutable_plan_changed')
need(encoded(descriptor) == owner_descriptor, 'owner_descriptor_mismatch')
ccr,_,_=self._derive()
expected = {'contract_version':'SECRETS-EXEC-20261011-v1',
'operation_id':meta['request']['operation_id'],
'operation':'metadata-apply-ccr','execution_actor':'service:secrets-engine',
'ccr_id':ccr['id'],'catalog_sha256':meta['request']['catalog_sha256'],
'operations':meta['request']['operations']}
need(all(descriptor.get(k)==v for k,v in expected.items()),'execution_descriptor_plan_mismatch')
bound = dict(meta, execution_descriptor=descriptor,
execution_descriptor_sha256=tagged(owner_descriptor))
if self.grant_record is not None:
bound['execution_grant']=self._bind_grant(plan,descriptor)
bound['grant_immutable_sha256']=descriptor['grant_immutable_sha256']
# Without an independently pinned grant this remains preparation only.
# The native fixture adapter refuses to materialize such an unbound plan.
return Plan(encoded(bound))
def before_call(self,plan,ordinal):
need(type(plan) is Plan and type(ordinal) is int and 0<=ordinal<6,'fixed_plan_ordinal_required')
try:
meta=plan.metadata();fresh=self.compile(meta['request'])
if 'execution_descriptor' in meta:
fresh=self.bind_execution(fresh,meta['execution_descriptor'],encoded(meta['execution_descriptor']))
except Refused: raise
except BaseException: raise Refused('invalid_plan') from None
need(fresh.serialized==plan.serialized,'immutable_plan_changed')
# Immediately re-derive owner payload bytes, and reconfirm immutable snapshot after derivation.
_,_,calls=self._derive()
final=self.compile(meta['request'])
if 'execution_descriptor' in meta:
final=self.bind_execution(final,meta['execution_descriptor'],encoded(meta['execution_descriptor']))
need(final.serialized==plan.serialized,'immutable_plan_changed')
return calls[ordinal]