196 lines
13 KiB
Python
196 lines
13 KiB
Python
|
|
"""Exact actual source successor path; legacy fixture evidence remains separate."""
|
||
|
|
from contextlib import contextmanager
|
||
|
|
from dataclasses import replace
|
||
|
|
from datetime import datetime,timedelta,timezone
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
import signal
|
||
|
|
import sys
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
from types import SimpleNamespace
|
||
|
|
import uuid
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts'))
|
||
|
|
from test_task_execution_native_fixture import owner,bao_dev,compiler
|
||
|
|
from task_execution_trusted_servers import trusted_providers,install_exact_owner_descriptor,http,private
|
||
|
|
import task_execution_trusted_servers as providers_source
|
||
|
|
from task_execution_mediator import run_native_fixture_task
|
||
|
|
from task_execution_native_fixture import NativeFixtureAdapter,_absolute_budget
|
||
|
|
from task_execution_journal import Refused
|
||
|
|
from secrets_engine.approval_claim import binding_from_check_request
|
||
|
|
from secrets_engine.approval_consume import _expected_request
|
||
|
|
from secrets_engine.authorization import approval_binding_digest
|
||
|
|
from secrets_engine.execution_contract import ExecutionContract
|
||
|
|
|
||
|
|
|
||
|
|
@contextmanager
|
||
|
|
def whole(owner,directory,*,window_seconds=60):
|
||
|
|
# OpenBao, Approval HTTPS, TokenReview TLS and actual Access CLI are all up
|
||
|
|
# before proposing an immutable descriptor/deadline or issuing approval.
|
||
|
|
with trusted_providers(directory) as provider:
|
||
|
|
native,journal,binding,call,compiler,descriptor=install_exact_owner_descriptor(owner,window_seconds=window_seconds)
|
||
|
|
snapshot={path:compiler.read(Path(path)) for path in compiler.pins}
|
||
|
|
entry=compiler._catalogue_entry(snapshot)
|
||
|
|
approval_id=str(uuid.uuid4())
|
||
|
|
entry=replace(entry,approval={**entry.approval,'authorization_id':approval_id})
|
||
|
|
request=_expected_request(provider.cfg,entry,'execute-ccr',execution_contract=ExecutionContract.parse(descriptor))
|
||
|
|
from secrets_engine.task_execution_handoff import prepare_task_approval
|
||
|
|
preparation=prepare_task_approval(provider.cfg,entry,execution_contract=ExecutionContract.parse(descriptor))
|
||
|
|
normalization=preparation.as_owner_handoff()
|
||
|
|
assert normalization['original_request']==request
|
||
|
|
assert 'attributes' not in request['subject']
|
||
|
|
assert normalization['approval_binding_digest']!=normalization['submitted_request_digest']
|
||
|
|
now=datetime.now(timezone.utc)
|
||
|
|
body={'id':approval_id,'binding':normalization['native_binding'],'pdp_digest':normalization['approval_binding_digest'],
|
||
|
|
'pdp_path':True,'human_control':True,'required_count':1,
|
||
|
|
'validity':{'not_before':(now-timedelta(minutes=1)).isoformat(),'expires_at':(now+timedelta(minutes=3)).isoformat()}}
|
||
|
|
assert http(provider.cfg.approval_url,'POST','/v1/approvals',body,'synthetic-whole-service')[0]==201
|
||
|
|
assert http(provider.cfg.approval_url,'POST','/v1/approvals/'+approval_id+'/entries',{},provider.human_token)[0]==200
|
||
|
|
yield SimpleNamespace(native=native,journal=journal,call=call,compiler=compiler,descriptor=descriptor,
|
||
|
|
entry=entry,provider=provider,cfg=provider.cfg,approval_id=approval_id,request=request,normalization=normalization,preparation=preparation)
|
||
|
|
|
||
|
|
|
||
|
|
def invoke(fixture):
|
||
|
|
return run_native_fixture_task(compiler=fixture.compiler,journal=fixture.journal,
|
||
|
|
plan_request=fixture.native.meta['request'],owner_descriptor=fixture.native.admission.descriptor,
|
||
|
|
secrets_cfg=fixture.cfg,entry=fixture.entry,confirmation=fixture.provider.confirmation,
|
||
|
|
native_admission=fixture.native.admission)
|
||
|
|
|
||
|
|
|
||
|
|
def untouched(fixture):
|
||
|
|
assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)==(404,{'errors':[]})
|
||
|
|
for ordinal in [1,4]:
|
||
|
|
path=fixture.compiler.before_call(fixture.native.plan,ordinal).path
|
||
|
|
assert fixture.call('GET',path)==(404,{'errors':[]})
|
||
|
|
|
||
|
|
|
||
|
|
def test_actual_successor_allow_strict_cas_confirm_six_calls_readback_cleanup(owner,tmp_path,monkeypatch):
|
||
|
|
with whole(owner,tmp_path) as fixture:
|
||
|
|
from secrets_engine.task_execution_handoff import consume_task_execution
|
||
|
|
with pytest.raises(Exception):consume_task_execution(fixture.cfg,fixture.preparation)
|
||
|
|
assert not any(item==('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume') for item in fixture.provider.calls)
|
||
|
|
trace=[];private_task=[];issued_native=[]
|
||
|
|
original=NativeFixtureAdapter._http
|
||
|
|
def observed(native,method,path,token,body=b''):
|
||
|
|
result=original(native,method,path,token,body)
|
||
|
|
trace.append((method,path,result[0]))
|
||
|
|
if method=='POST' and path=='auth/token/create' and result[0]==200:
|
||
|
|
private_task.append((result[1]['auth']['accessor'],result[1]['auth']['client_token']))
|
||
|
|
issued_native.append(native)
|
||
|
|
return result
|
||
|
|
monkeypatch.setattr(NativeFixtureAdapter,'_http',observed)
|
||
|
|
try:result=invoke(fixture)
|
||
|
|
except Exception as error:
|
||
|
|
from urllib.error import HTTPError
|
||
|
|
cause=error.__cause__
|
||
|
|
if isinstance(cause,HTTPError):
|
||
|
|
raw=cause.read(4097)
|
||
|
|
known={b'trusted Approval source refused\n':'approval-source-correspondence',
|
||
|
|
b'service task execution input refused\n':'service-input-correspondence'}
|
||
|
|
diagnostic={'status':cause.code,'constant_reason':known.get(raw,'other-closed-classification'),
|
||
|
|
'native_operations':fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]}
|
||
|
|
(tmp_path/'closed-source-refusal.json').write_text(json.dumps(diagnostic))
|
||
|
|
print(json.dumps(diagnostic))
|
||
|
|
raise
|
||
|
|
assert result['native_apply']=='verified' and result['native_calls_completed']==6
|
||
|
|
assert result['cleanup_confirmed'] and result['business_state']=='cleanup-confirmed'
|
||
|
|
assert fixture.provider.engine.get(fixture.approval_id).status=='consumed'
|
||
|
|
records=[json.loads(line) for line in fixture.provider.record.read_bytes().splitlines()]
|
||
|
|
assert len(records)==2 and [x['effect'] for x in records]==['deny','allow']
|
||
|
|
assert all(x['signature']['mode']=='signed' for x in records)
|
||
|
|
normalization_record,record=records
|
||
|
|
assert normalization_record['id']==fixture.normalization['decision_id']
|
||
|
|
assert record['id']==result['decision_id']
|
||
|
|
assert record['lifetime']['expires_at']==fixture.descriptor['operation_deadline']
|
||
|
|
assert record['provenance']['policy_package']=='secrets-engine.task-execution.service-pilot'
|
||
|
|
assert record['provenance']['policy_version']=='v2'
|
||
|
|
assert record['provenance']['approval_observation']['source_id']=='synthetic-whole-actual-approval'
|
||
|
|
assert record['binding']['context']['execution_contract']==fixture.descriptor
|
||
|
|
native_calls=fixture.journal.db.execute('SELECT ordinal,outcome FROM calls ORDER BY ordinal').fetchall()
|
||
|
|
assert native_calls[:2]==[(0,'complete'),(1,'complete')] and len(native_calls)==13
|
||
|
|
assert all(value=='complete' for _,value in native_calls)
|
||
|
|
assert fixture.provider.calls.count(('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1
|
||
|
|
assert fixture.provider.calls.count(('consumer','GET','/v1/approvals/'+fixture.approval_id))==1
|
||
|
|
assert any(item==('source','GET','/v1/approvals/'+fixture.approval_id+'/claim') for item in fixture.provider.calls)
|
||
|
|
expected=fixture.compiler.proposal()['operations']
|
||
|
|
assert [(method,path) for method,path,_ in trace if path in {item['path'] for item in expected}]==[(x['method'],x['path']) for x in expected]
|
||
|
|
assert len(private_task)==1
|
||
|
|
assert issued_native[0]._task_wall_deadline<issued_native[0].deadline
|
||
|
|
assert issued_native[0]._task_monotonic_deadline<issued_native[0].monotonic_deadline
|
||
|
|
for accessor in [private_task[0][0],fixture.native.admission.identity_session.accessor]:
|
||
|
|
assert fixture.call('POST','auth/token/lookup-accessor',{'accessor':accessor})[0]==400
|
||
|
|
assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)==(404,{'errors':[]})
|
||
|
|
raw=(fixture.journal.directory/'journal.sqlite').read_bytes()
|
||
|
|
for secret in [*private_task[0],fixture.native.admission.owner_token,fixture.native.admission.identity_session.token]:assert secret.encode() not in raw
|
||
|
|
before=list(trace)
|
||
|
|
with pytest.raises(Exception):invoke(fixture)
|
||
|
|
assert trace==before
|
||
|
|
assert fixture.provider.calls.count(('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume'))==1
|
||
|
|
# Public evidence is an allowlist: never serialize native admission,
|
||
|
|
# credentials, private handles, arbitrary errors or provider config.
|
||
|
|
evidence={'schema':'railiance-platform.disposable-wholechain-proof.v1',
|
||
|
|
'source_effects':[x['effect'] for x in records],
|
||
|
|
'signed_source_records':all(x['signature']['mode']=='signed' for x in records),
|
||
|
|
'retained_registry_normalized':fixture.normalization['approval_binding_digest']!=fixture.normalization['submitted_request_digest'],
|
||
|
|
'approval_state':fixture.provider.engine.get(fixture.approval_id).status,
|
||
|
|
'consume_count':1,'independent_confirmation_get_count':1,
|
||
|
|
'native_calls_completed':result['native_calls_completed'],
|
||
|
|
'business_state':result['business_state'],'cleanup_confirmed':result['cleanup_confirmed'],
|
||
|
|
'durable_completed_calls':len(native_calls),'retry_refused':True,
|
||
|
|
'credential_expiry_narrows_operation_deadline':True,
|
||
|
|
'private_material_absent_from_public_journal':True,
|
||
|
|
'immediate_cas':True,'artificial_clock_or_cas_wait':False,
|
||
|
|
'production_admission':False}
|
||
|
|
(tmp_path/'wholechain-public-receipt.json').write_text(json.dumps(evidence,indent=2)+'\n')
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize('case',['source-drift','foreign-caller','revoked','source-reader-denied','registry-drift'])
|
||
|
|
def test_actual_successor_refusals_never_consume_or_touch_backend(owner,tmp_path,case):
|
||
|
|
with whole(owner,tmp_path) as fixture:
|
||
|
|
expected_paths=[fixture.compiler.before_call(fixture.native.plan,x).path for x in [1,4]]
|
||
|
|
if case=='source-drift':
|
||
|
|
fixture.compiler.validator.write_bytes(fixture.compiler.validator.read_bytes()+b'\n# changed source\n')
|
||
|
|
elif case=='foreign-caller':fixture.provider.state.principal='system:serviceaccount:other:other'
|
||
|
|
elif case=='revoked':
|
||
|
|
assert http(fixture.cfg.approval_url,'POST','/v1/approvals/'+fixture.approval_id+'/revoke',{},fixture.provider.human_token)[0]==200
|
||
|
|
elif case=='source-reader-denied':private(fixture.provider.reader_file,'synthetic-unknown-reader')
|
||
|
|
elif case=='registry-drift':fixture.provider.restart_registry()
|
||
|
|
with pytest.raises(Exception):invoke(fixture)
|
||
|
|
assert not any(item==('consumer','POST','/v1/approvals/'+fixture.approval_id+'/consume') for item in fixture.provider.calls)
|
||
|
|
assert fixture.journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]==0
|
||
|
|
records=[json.loads(line) for line in fixture.provider.record.read_bytes().splitlines()]
|
||
|
|
assert [x['effect'] for x in records]==['deny']
|
||
|
|
assert fixture.call('GET','sys/policies/acl/'+fixture.native.policy_name)==(404,{'errors':[]})
|
||
|
|
for path in expected_paths:assert fixture.call('GET',path)==(404,{'errors':[]})
|
||
|
|
|
||
|
|
|
||
|
|
def test_actual_revoke_between_prepare_and_cas_keeps_native_untouched(owner,tmp_path,monkeypatch):
|
||
|
|
import secrets_engine.task_execution_handoff as strict
|
||
|
|
with whole(owner,tmp_path) as fixture:
|
||
|
|
original=strict.consume_task_execution
|
||
|
|
def revoked(cfg,authorization):
|
||
|
|
assert http(cfg.approval_url,'POST','/v1/approvals/'+fixture.approval_id+'/revoke',{},fixture.provider.human_token)[0]==200
|
||
|
|
return original(cfg,authorization)
|
||
|
|
monkeypatch.setattr(strict,'consume_task_execution',revoked)
|
||
|
|
with pytest.raises(Refused,match='confirmation unconfirmed'):invoke(fixture)
|
||
|
|
assert fixture.journal.db.execute('SELECT state FROM operations').fetchone()[0]=='unknown'
|
||
|
|
untouched(fixture)
|
||
|
|
assert fixture.provider.engine.get(fixture.approval_id).status=='revoked'
|
||
|
|
assert [json.loads(line)['effect'] for line in fixture.provider.record.read_bytes().splitlines()]==['deny','allow']
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
def test_expired_contract_refuses_preparation_before_approval_or_native(owner,tmp_path):
|
||
|
|
from secrets_engine.task_execution_handoff import prepare_task_approval
|
||
|
|
with trusted_providers(tmp_path) as provider:
|
||
|
|
native,journal,binding,call,compiler,descriptor=install_exact_owner_descriptor(owner,window_seconds=-1)
|
||
|
|
snapshot={path:compiler.read(Path(path)) for path in compiler.pins}
|
||
|
|
entry=compiler._catalogue_entry(snapshot)
|
||
|
|
with pytest.raises(Exception):prepare_task_approval(provider.cfg,entry,execution_contract=ExecutionContract.parse(descriptor))
|
||
|
|
assert not provider.record.exists() or not provider.record.read_bytes()
|
||
|
|
assert not any(len(item)==3 and item[1]=='POST' for item in provider.calls)
|
||
|
|
assert journal.db.execute('SELECT COUNT(*) FROM operations').fetchone()[0]==0
|
||
|
|
assert call('GET','sys/policies/acl/'+native.policy_name)==(404,{'errors':[]})
|