railiance-platform/tests/test_task_execution_native_fixture.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

310 lines
18 KiB
Python

"""Actual disposable OpenBao effects; all identities/approvals are synthetic.
These tests do not contact estate OpenBao or claim production owner acceptance.
"""
import hashlib
from dataclasses import replace
import yaml
import http.client
import json
import shutil
import socket
import subprocess
import time
from datetime import datetime,timedelta,timezone
from pathlib import Path
import sys
import pytest
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
from test_task_execution_plan import compiler,request,m
from task_execution_journal import Binding,Cleanup,Journal,Refused
from task_execution_cleanup import run_cleanup
from task_execution_native_fixture import FixtureAdmission,NativeFixtureAdapter,PrivateSession
import task_execution_grant as grant
@pytest.fixture
def bao_dev():
executable=shutil.which('bao')
if not executable: pytest.skip('OpenBao executable unavailable')
with socket.socket() as sock:
sock.bind(('127.0.0.1',0));port=sock.getsockname()[1]
root='synthetic-disposable-fixture-root'
process=subprocess.Popen([executable,'server','-dev','-dev-no-store-token',
'-dev-root-token-id='+root,'-dev-listen-address=127.0.0.1:'+str(port)],
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
def call(method,path,body=None,token=root):
conn=http.client.HTTPConnection('127.0.0.1',port,timeout=2)
try:
conn.request(method,'/v1/'+path,body=json.dumps(body) if body is not None else None,
headers={'X-Vault-Token':token,'Content-Type':'application/json'})
response=conn.getresponse();raw=response.read()
return response.status,json.loads(raw) if raw else {}
finally: conn.close()
for _ in range(100):
if process.poll() is not None: pytest.fail('disposable backend exited')
try:
if call('GET','sys/health')[0]==200: break
except OSError: pass
time.sleep(0.05)
else:
process.kill();process.wait();pytest.fail('disposable backend failed to start')
try:
yield 'http://127.0.0.1:'+str(port),root,call,process
finally:
process.terminate()
try: process.wait(timeout=5)
except subprocess.TimeoutExpired: process.kill();process.wait()
@pytest.fixture
def owner(compiler,bao_dev,tmp_path):
origin,root,call,process=bao_dev
assert call('POST','sys/auth/kubernetes',{'type':'kubernetes'})[0] in {200,204}
hcl='path "auth/token/revoke-self" { capabilities = ["update"] }'
assert call('PUT','sys/policies/acl/fixture-self-only',{'policy':hcl})[0] in {200,204}
status,result=call('POST','auth/token/create',{'policies':['fixture-self-only'],'no_default_policy':True,'renewable':False,'ttl':'300s','num_uses':8})
assert status==200
identity=PrivateSession(result['auth']['client_token'],result['auth']['accessor'])
req=request(compiler);plan=compiler.compile(req)
descriptor={'contract_version':'SECRETS-EXEC-20261011-v1','operation_id':req['operation_id'],
'operation':'metadata-apply-ccr','execution_actor':'service:secrets-engine',
'ccr_id':compiler._derive()[0]['id'],'ccr_immutable_sha256':req['ccr_sha256'],
'execution_grant_ccr_id':'FIXTURE-GRANT-NOT-PRODUCTION','grant_immutable_sha256':'sha256:'+'f'*64,
'catalog_sha256':req['catalog_sha256'],
'source_bundle':{role:{'path':'/fixture/'+role,'sha256':'sha256:'+'d'*64} for role in ['engine_dispatcher','platform_mediator','typed_ccr','policy_auth']},
'backend_origin':origin,'cluster_uid':'disposable-fixture-only',
'operations':req['operations'],'operation_deadline':(datetime.now(timezone.utc)+timedelta(seconds=240)).isoformat(),
'backend_budget':{'ttl_seconds':120,'uses':12},'expected_before':'sha256:'+'0'*64,'expected_after':'sha256:'+'1'*64}
# Exercise the actual consumer closed descriptor parser as well; source pins,
# grant/expected-state digests remain deliberately synthetic owner fixtures.
from secrets_engine.execution_contract import ExecutionContract
descriptor=ExecutionContract.parse(descriptor).as_dict()
# Install an independently pinned synthetic grant source and its pure
# validator. No real CCR is written or approved in the owner checkout.
grantfile=compiler.config.platform_repo/'credential-change-requests/CCR-FIXTURE-GRANT.yaml'
grantfile.write_text('placeholder: true\n')
grantcode=Path(grant.__file__).resolve()
pins=dict(compiler.config.source_pins)
pins.update({str(grantfile):m.tagged(grantfile.read_bytes()),str(grantcode):m.tagged(grantcode.read_bytes())})
compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items()),grant_filename=grantfile.name))
plan=compiler.compile(req)
expected_execution,expected_openbao=compiler.grant_effects(plan,descriptor)
record={'id':descriptor['execution_grant_ccr_id'],'kind':'credential-change-request',
'schema_version':1,'request_type':'task-execution-grant','title':'Synthetic disposable fixture grant',
'status':'approved','created':'2026-10-16','updated':'2026-10-16',
'requester':{'agent':'fixture','reason':'Synthetic disposable proof; no founder approval'},
'review':{'required':True,'required_approvers':['railiance-platform-owner','secrets-engine-owner','access-engine-owner','approval-engine-owner'],'comments':[]},
'target':{'domain':'infotech','tenant':'platform','workload':'secrets-engine','environment':'production','purpose':'Synthetic metadata primitive proof'},
'execution':expected_execution,'openbao':expected_openbao,'evidence':{'events':[]}}
grantfile.write_text(yaml.safe_dump(record,sort_keys=False))
pins[str(grantfile)]=m.tagged(grantfile.read_bytes())
compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
descriptor['grant_immutable_sha256']=grant.immutable_digest(record)
installed=m.encoded(descriptor)
plan=compiler.bind_execution(compiler.compile(req),descriptor,installed)
binding=Binding('SYNTHETIC-APPROVAL-NOT-PRODUCTION',req['request_digest'],req['operation_id'],plan.content_digest)
state=tmp_path/'private-journal';state.mkdir(mode=0o700);journal=Journal(state)
admission=FixtureAdmission(installed,root,identity,process)
native=NativeFixtureAdapter(compiler,plan,journal,binding,admission)
try: yield native,journal,binding,call,compiler,descriptor
finally: journal.close()
def admit(native,journal,binding):
# Synthetic postconsume transition is fixture setup, not served approval proof.
journal.reserve(binding,native.cleanup)
journal.transition(binding,'reserved','consume-confirmed')
journal.transition(binding,'consume-confirmed','authenticating')
journal.transition(binding,'authenticating','grant-issuing')
def test_native_fixed_grant_metadata_denials_and_confirmed_cleanup(owner):
native,journal,binding,call,compiler,descriptor=owner
with journal.locked():
admit(native,journal,binding)
info=native.materialize_grant()
assert info['issued'] and info['ttl_ceiling_seconds']==120 and info['use_ceiling']==12
journal.transition(binding,'grant-issued','applying')
baseline=native.metadata_call(0);assert baseline[0]==404
assert native.metadata_call(1)[0] in {200,204}
policy=native.metadata_call(2);assert policy[0]==200
assert policy[1]['data']['policy'].rstrip()==json.loads(compiler.before_call(native.plan,1).payload)['policy'].rstrip()
assert native.metadata_call(3)[0]==404
assert native.metadata_call(4)[0] in {200,204}
role=native.metadata_call(5);assert role[0]==200
# Actual native role metadata correspondence for fixture's known K8s body.
expected=json.loads(compiler.before_call(native.plan,4).payload)
assert role[1]['data']['bound_service_account_names']==expected['bound_service_account_names']
assert role[1]['data']['bound_service_account_namespaces']==expected['bound_service_account_namespaces']
assert role[1]['data']['token_policies']==expected['policies'].split(',')
# Root probes native capabilities without spending the limited task token.
for path in ['sys/policies/acl/sibling','auth/kubernetes/role/sibling','secret/data/sibling','auth/token/create','sys/mounts/secret','auth/approle/role/sibling/secret-id']:
status,data=call('POST','sys/capabilities',{'token':native.task.token,'paths':[path]})
assert status==200 and data['capabilities']==['deny']
task_accessor=native.task.accessor;identity_accessor=native.admission.identity_session.accessor
result=run_cleanup(journal,binding,native)
# Assert native absence before summary so fixture failures identify protocol semantics.
for accessor in [task_accessor,identity_accessor]:
status,data=call('POST','auth/token/lookup-accessor',{'accessor':accessor})
assert status==400 and data.get('errors') in (['invalid accessor'],['1 error occurred:\n\t* invalid accessor\n\n'])
assert result['confirmed']
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
assert journal.observe(binding)['state']=='applying' # native success is not full owner verification
with pytest.raises(Refused): native.metadata_call(6)
with pytest.raises(Refused): run_cleanup(journal,binding,native)
def test_no_consume_no_native_grant_and_changed_digest_no_cleanup(owner):
native,journal,binding,call,*_=owner
with journal.locked():
journal.reserve(binding,native.cleanup)
with pytest.raises(Refused):native.materialize_grant()
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
with pytest.raises(Refused):native.delete_grant_policy('0'*64)
def test_native_cleanup_after_deadline_and_pending_business_uncertainty(owner):
native,journal,binding,call,*_=owner
with journal.locked():
admit(native,journal,binding);native.materialize_grant()
journal.transition(binding,'grant-issued','applying')
journal.before_call(binding,native._ordinal,'a'*64)
journal.transition(binding,'applying','unknown')
native.monotonic_deadline=time.monotonic()-1
with pytest.raises(Refused):native.metadata_call(0)
assert run_cleanup(journal,binding,native)['confirmed']
assert journal.observe(binding)['state']=='unknown'
assert journal.observe(binding)['external_outcome_unknown']
def test_whole_descriptor_and_cleanup_commitment_are_bound(owner):
native,journal,binding,call,compiler,descriptor=owner
base=compiler.compile(native.meta['request'])
changed=dict(descriptor,cluster_uid='different-cluster')
with pytest.raises(m.Refused):compiler.bind_execution(base,changed,native.admission.descriptor)
data=native.plan.metadata();data['cleanup'][0]['call_digest']='9'*64
with pytest.raises(m.Refused):compiler.before_call(m.Plan(m.encoded(data)),0)
with pytest.raises(Refused):NativeFixtureAdapter(compiler,m.Plan(m.encoded(data)),journal,binding,native.admission)
def test_source_drift_after_admission_prevents_mint_but_allows_native_cleanup(owner):
native,journal,binding,call,compiler,*_=owner
with journal.locked():
admit(native,journal,binding)
compiler.ccr.write_bytes(compiler.ccr.read_bytes()+b'\n# changed owner source\n')
with pytest.raises(m.Refused):native.materialize_grant()
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
assert run_cleanup(journal,binding,native)['confirmed']
def test_actual_native_write_then_lost_response_stays_unknown_and_cleans(owner):
native,journal,binding,call,compiler,*_=owner
with journal.locked():
admit(native,journal,binding);native.materialize_grant()
journal.transition(binding,'grant-issued','applying')
assert native.metadata_call(0)[0]==404
original=native._http
target=compiler.before_call(native.plan,1).path
def lost_response(method,path,token,body=b''):
result=original(method,path,token,body)
if method=='PUT' and path==target:
raise Refused('synthetic lost response after real native write')
return result
native._http=lost_response
with pytest.raises(Refused):native.metadata_call(1)
assert call('GET',target)[0]==200 # actual side effect despite missing confirmation
assert journal.observe(binding)['state']=='unknown'
assert journal.observe(binding)['external_outcome_unknown']
assert run_cleanup(journal,binding,native)['confirmed']
assert journal.observe(binding)['state']=='unknown'
with pytest.raises(Refused):native.metadata_call(1)
assert call('GET',target)[0]==200 # cleanup never rolls back/reapplies business state
def test_actual_native_policy_retirement_does_not_prove_failed_token_revoke(owner):
native,journal,binding,call,*_=owner
with journal.locked():
admit(native,journal,binding);native.materialize_grant()
original=native._http
def refuse_task_revoke(method,path,token,body=b''):
if method=='POST' and path=='auth/token/revoke-self' and token==native.task.token:
return 503,{'errors':['synthetic cleanup transport failure']}
return original(method,path,token,body)
native._http=refuse_task_revoke
result=run_cleanup(journal,binding,native)
assert not result['confirmed']
assert [item['outcome'] for item in result['obligations']]==['unconfirmed','confirmed','confirmed']
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
# Actual owner lookup demonstrates token still exists despite deleted ACL.
status,data=call('POST','auth/token/lookup-accessor',{'accessor':native.task.accessor})
assert status==200 and data['data']['policies']==[native.policy_name]
with pytest.raises(Refused):run_cleanup(journal,binding,native)
# Explicit synthetic owner recovery, not adapter retry/resumption.
assert call('POST','auth/token/revoke-accessor',{'accessor':native.task.accessor})[0] in {200,204}
def test_nonloopback_estate_origin_is_refused_before_native_transport(owner):
native,journal,binding,call,compiler,descriptor=owner
changed=dict(descriptor,backend_origin='https://estate.invalid:8200')
# The owner grant now also binds origin. Build a matching synthetic grant so
# this case reaches the adapter's independent nonloopback refusal; ordinary
# origin substitution is already refused earlier by the grant join.
record=yaml.safe_load(compiler.grant_record.read_text())
record['execution'],record['openbao']=compiler.grant_effects(compiler.compile(native.meta['request']),changed)
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed['grant_immutable_sha256']=grant.immutable_digest(record)
installed=m.encoded(changed)
plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed,installed)
changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest)
admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
def test_changed_grant_immutable_join_refuses_before_any_native_grant(owner):
native,journal,binding,call,compiler,descriptor=owner
changed=dict(descriptor,grant_immutable_sha256='sha256:'+'0'*64)
with pytest.raises(m.Refused):compiler.bind_execution(compiler.compile(native.meta['request']),changed,m.encoded(changed))
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_grant_body_repin_cannot_replace_independently_derived_effects(owner):
native,journal,binding,call,compiler,descriptor=owner
record=yaml.safe_load(compiler.grant_record.read_text())
record['openbao']['policy']['hcl']='path "*" { capabilities = ["sudo"] }'
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed_descriptor=dict(descriptor,grant_immutable_sha256=grant.immutable_digest(record))
with pytest.raises(m.Refused):changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed_descriptor,m.encoded(changed_descriptor))
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_proposed_source_grant_never_materializes_even_with_matching_digest(owner):
native,journal,binding,call,compiler,descriptor=owner
record=yaml.safe_load(compiler.grant_record.read_text());record['status']='proposed'
compiler.grant_record.write_text(yaml.safe_dump(record))
pins=dict(compiler.config.source_pins);pins[str(compiler.grant_record)]=m.tagged(compiler.grant_record.read_bytes())
changed_compiler=m.Compiler(replace(compiler.config,source_pins=tuple(pins.items())))
changed_descriptor=dict(descriptor,grant_immutable_sha256=grant.immutable_digest(record))
installed=m.encoded(changed_descriptor)
plan=changed_compiler.bind_execution(changed_compiler.compile(native.meta['request']),changed_descriptor,installed)
changed_binding=Binding(binding.approval_id,binding.request_digest,binding.operation_id,plan.content_digest)
admission=FixtureAdmission(installed,native.admission.owner_token,native.admission.identity_session,native.admission.server_process)
with pytest.raises(Refused):NativeFixtureAdapter(changed_compiler,plan,journal,changed_binding,admission)
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
def test_grant_source_drift_after_admission_prevents_protected_effect(owner):
native,journal,binding,call,compiler,*_=owner
with journal.locked():
admit(native,journal,binding)
record=yaml.safe_load(compiler.grant_record.read_text());record['execution']['cleanup'][0]['call_digest']='f'*64
compiler.grant_record.write_text(yaml.safe_dump(record))
with pytest.raises(m.Refused):native.materialize_grant()
assert call('GET','sys/policies/acl/'+native.policy_name)[0]==404
assert run_cleanup(journal,binding,native)['confirmed']