fix(auth): authenticate producer-signed decisions before authorization
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
tegwick 2026-10-16 07:51:44 +02:00
parent 6bd7129eac
commit c80a944105
30 changed files with 2059 additions and 32 deletions

View file

@ -0,0 +1,118 @@
{
"id": "decision:414734bb30381ff7",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-rotate",
"effect": "allow",
"reason": "catalog_lane_policy_matched",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"binding": {
"tenant": "tenant:platform",
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"action": "rotate",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
},
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-07T07:10:23Z",
"expires_at": "2026-09-07T07:25:23Z"
},
"diagnostics": {
"action": "rotate",
"matched_relationship": "",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_status": "ready",
"registry_overrode": [],
"registry_resource": false,
"registry_subject": true
},
"provenance": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-07T07:10:23Z"
},
"signature": {
"mode": "signed",
"alg": "ed25519",
"kid": "testdata-ed25519",
"value": "G45R7eb_7Dl7B8RO5HTgefcys6QabYwe7NZcF4ju_zpEAfEBWKVK5T5e5rP1GNJ5uuVusrw6Fd90tttg3_-CAQ"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"conformance_findings": [
{
"code": "CARING-DESCRIPTOR-MISSING",
"severity": "warning",
"message": "no CARING descriptor matched the request",
"fields": [
"caring_context"
]
}
]
}
}

View file

@ -0,0 +1,118 @@
{
"id": "decision:414734bb30381ff7",
"contract_version": "flex-auth.decision-record.v1",
"request_id": "check:secrets-engine-rotate",
"effect": "deny",
"reason": "tampered_after_signing",
"matched_policy_version": "v2",
"matched_rule": "catalog_lane_policy_matched",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"binding": {
"tenant": "tenant:platform",
"subject": {
"id": "secrets-engine",
"type": "service",
"tenant": "tenant:platform",
"attributes": {
"description": "secrets-engine's own service identity, the single calling identity for the twelve gated catalog-lane actions it sends to POST /v1/check. Because it is the only subject, the package has no action_not_granted branch (FLEX-WP-0021-T02); registering a second identity is the revisit trigger.",
"display_name": "secrets-engine service principal",
"groups": [
"group:secrets-engine-lane-operators"
],
"organization_relation": "ServiceProvider",
"roles": [
"Operator"
]
}
},
"action": "rotate",
"resource": {
"id": "lane:glas-primary",
"type": "secret-catalog-lane",
"system": "secrets-engine",
"tenant": "tenant:platform",
"attributes": {
"auth_targets": [],
"fields": [
"password"
],
"policy_targets": [],
"stage": "prod"
}
},
"request_digest": "sha256:de67324f54187055307a833235f83ced9fcd3a20952a27b3d19493ed39734345",
"submitted_request_digest": "sha256:41c8fc084e58c46554ccb6afe9943a99906e5986668c923811721f66d9b30a6a"
},
"lifetime": {
"kind": "ttl",
"ttl": "15m",
"not_before": "2026-09-07T07:10:23Z",
"expires_at": "2026-09-07T07:25:23Z"
},
"diagnostics": {
"action": "rotate",
"matched_relationship": "",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_status": "ready",
"registry_overrode": [],
"registry_resource": false,
"registry_subject": true
},
"provenance": {
"evaluator": "flex-auth/local",
"mode": "standalone",
"policy_package": "secrets-engine.catalog-lane.lifecycle",
"policy_version": "v2",
"policy_package_digest": "sha256:bd11c5fe77ce6439c65fea225ad6b71d2110efc5e7b5bc9b499c59cd0a53b8b4",
"registry_snapshot_digest": "sha256:f5a309bc0b36721fd6d9ad7f53eb21222162bc2eac62a0ab0802a9a1d51340bb",
"decision_time": "2026-09-07T07:10:23Z"
},
"signature": {
"mode": "signed",
"alg": "ed25519",
"kid": "testdata-ed25519",
"value": "G45R7eb_7Dl7B8RO5HTgefcys6QabYwe7NZcF4ju_zpEAfEBWKVK5T5e5rP1GNJ5uuVusrw6Fd90tttg3_-CAQ"
},
"caring": {
"profile": "caring-0.4.0-rc2",
"conformance_findings": [
{
"code": "CARING-DESCRIPTOR-MISSING",
"severity": "warning",
"message": "no CARING descriptor matched the request",
"fields": [
"caring_context"
]
}
]
}
}

View file

@ -0,0 +1,35 @@
"""Native producer signing fixtures, never an authentication bypass."""
import json
import os
from pathlib import Path
import subprocess
import pytest
PUBLIC='IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'
@pytest.fixture(scope='session')
def signature_tools(tmp_path_factory):
directory=tmp_path_factory.mktemp('signature-tools')
# Private writable build cache keeps tests runnable in the workspace sandbox.
env={**os.environ,'GOCACHE':os.environ.get('GOCACHE',str(directory/'cache'))}
root=Path(__file__).parents[1]/'verifier'
for name,package in [('verify','.'),('sign','./testsupport/signer')]:
subprocess.run(['go','build','-o',str(directory/name),package],cwd=root,env=env,check=True,timeout=120)
return directory
def admit_tools(cfg, directory, tools):
trust={'schema':'secrets-engine.pdp-trust.v1','evaluator':'stub/local','mode':'standalone',
'policy_package':cfg.authorization_policy_package,'policy_version':cfg.authorization_policy_version,
'policy_package_digest':'sha256:'+'1'*64,'keys':{'testdata-ed25519':PUBLIC}}
path=directory/'pdp-trust.json';path.write_text(json.dumps(trust))
from dataclasses import is_dataclass, replace
if is_dataclass(cfg):
return replace(cfg,pdp_trust_file=path,pdp_verifier=tools/'verify')
cfg.pdp_trust_file=path;cfg.pdp_verifier=tools/'verify'
return cfg
def sign_envelope(envelope,tools,next_key=False):
result=subprocess.run([str(tools/'sign')]+(['next'] if next_key else []),input=json.dumps(envelope),text=True,capture_output=True,check=True,timeout=3)
return json.loads(result.stdout)

View file

@ -194,8 +194,11 @@ def test_environment_selects_separate_approval_secret(monkeypatch):
assert cfg.keycape_client_secret_file == Path("/protected/openbao.secret")
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.mark.parametrize("failure", [None, "exchange", "conflict", "wrong-action"])
def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure):
def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure, signature_tools):
raw = copy.deepcopy(VALID)
raw["stage"] = "prod"
raw["approval"] = {"model": "bootstrap-only", "authorization_id": "approval-test", "purpose": "synthetic proof"}
@ -205,6 +208,9 @@ def test_cli_gate_exchanges_for_claim_and_consume(tmp_path, monkeypatch, failure
cfg.authorization_subject_type = "service"
cfg.authorization_policy_package = "secrets-engine.catalog-lane.lifecycle"
cfg.authorization_policy_version = "v2"
admit_tools(cfg,tmp_path,signature_tools)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,"decision",lambda self,request:sign_envelope(original_decision(self,request),signature_tools))
cfg.evidence_dir = tmp_path / "evidence"
cfg.pdp_token_file = tmp_path / "pdp.token"
cfg.pdp_token_file.write_text("synthetic-pdp-token")

View file

@ -24,7 +24,19 @@ from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from tests.test_catalog import VALID
AUTH_ID = "8bfc20be-47a4-4fb0-97a2-bf0a920afad8"
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.fixture(autouse=True)
def admitted_native_signer(monkeypatch, tmp_path, signature_tools):
original = _Cfg.__init__
def configured(self, *a, **kw):
original(self,*a,**kw)
admit_tools(self,tmp_path,signature_tools)
monkeypatch.setattr(_Cfg,'__init__',configured)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,'decision',lambda self,request:sign_envelope(original_decision(self,request),signature_tools))
APPROVAL_ID = "8bfc20be-47a4-4fb0-97a2-bf0a920afad8"
class _Cfg:
@ -45,7 +57,7 @@ class _Cfg:
def _entry():
raw = copy.deepcopy(VALID)
raw["approval"] = dict(raw.get("approval") or {})
raw["approval"]["authorization_id"] = AUTH_ID
raw["approval"]["authorization_id"] = APPROVAL_ID
raw["approval"]["purpose"] = "contract-test"
return validate_entry(raw)
@ -76,7 +88,7 @@ def _served(entry=None, action="deactivate", fields=("api_token",), **over):
"schema_version": "0.1",
"kind": "approval-claim",
"issuer": "approval-engine",
"approval_id": AUTH_ID,
"approval_id": APPROVAL_ID,
"state": "valid",
"valid_now": True,
"consumed": False,
@ -116,7 +128,7 @@ def _opener(envelope, status=200):
def _resolve(cfg, entry, envelope, action="deactivate"):
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
def check(request, timeout=None):
submitted = json.loads(request.data)
return _opener(pdp.decision(submitted))(request)
@ -136,7 +148,7 @@ def test_valid_authorization_yields_binding_with_canonical_digest(tmp_path):
cfg = _Cfg(_token(tmp_path))
binding = _resolve(cfg, entry, _served())
assert binding is not None
assert binding.binding.approval_id == AUTH_ID
assert binding.binding.approval_id == APPROVAL_ID
assert binding.binding.request_digest != request_digest(_expected_request(entry))

View file

@ -176,7 +176,7 @@ def test_the_guard_runs_before_the_token_is_read(tmp_path):
"""
missing = tmp_path / "never-read.token"
with pytest.raises(DecisionError, match="in-cluster Service name"):
check_decision(
check_decision(allow_unsigned_test_fixture=True,
base_url="http://flex-auth-secrets-engine.flex-auth.svc.cluster.local:8080",
token_file=missing,
request={},

View file

@ -0,0 +1,110 @@
"""Producer-signed native replay and real consumer boundary regression checks."""
import copy
import json
from pathlib import Path
import subprocess
from types import SimpleNamespace
import pytest
from secrets_engine import approval_consume
from secrets_engine.authorization import validate_decision_envelope
from secrets_engine.decision_check import check_decision
from secrets_engine.decision_signature import load_trust
from secrets_engine.errors import DecisionError
from tests.test_decision_transport import server, credential, clear_proxies
FIXTURE = Path(__file__).parent / 'fixtures/pdp-signature/decision_rotate_signed.json'
PUBLIC = 'IVL40Zt5HSRFMkLhXy6rbLfP-ntqXtMAl5YOBpiB2xI'
from tests.signature_support import signature_tools
@pytest.fixture(scope='module')
def verifier(signature_tools):
return signature_tools/'verify'
@pytest.fixture
def trust_file(tmp_path):
envelope = json.loads(FIXTURE.read_bytes())
provenance = envelope['provenance']
trust = {k:provenance[k] for k in ('evaluator','mode','policy_package','policy_version','policy_package_digest')}
trust.update(schema='secrets-engine.pdp-trust.v1', keys={'testdata-ed25519':PUBLIC})
path = tmp_path/'trust.json'; path.write_text(json.dumps(trust)); return path
def checked(body, credential, trust_file, verifier):
with server(body=body) as (origin, received):
result = check_decision(base_url=origin, token_file=credential, request={},
trust_file=trust_file, verifier=verifier)
assert received == [True]
return result
def test_genuine_native_signature_at_consumer_boundary(credential, trust_file, verifier):
result = checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
assert result['effect']=='allow'
assert result['binding']==json.loads(FIXTURE.read_bytes())['binding']
@pytest.mark.parametrize('change', ['allow_forgery','tampered','unknown_key','unsigned','absent','algorithm','unknown_field','duplicate','null','case_alias'])
def test_response_authentication_refuses(credential, trust_file, verifier, change):
env=json.loads(FIXTURE.read_bytes())
if change=='allow_forgery': env['signature']['value']='A'*86
elif change=='tampered': env['binding']['action']='destroy'
elif change=='unknown_key': env['signature']['kid']='new-key'
elif change=='unsigned': env['signature']={'mode':'unsigned'}
elif change=='absent': del env['signature']
elif change=='algorithm': env['signature']['alg']='none'
elif change=='unknown_field': env['unbound_authority']='allow'
elif change=='null': env['binding']=None
elif change=='case_alias': env['Effect']='deny'
body=json.dumps(env).encode()
if change=='duplicate': body=body[:-1]+b',"effect":"deny"}'
with pytest.raises(DecisionError): checked(body,credential,trust_file,verifier)
@pytest.mark.parametrize('field',['evaluator','mode','policy_package','policy_version','policy_package_digest'])
def test_independent_issuer_and_pin_refusal(credential, trust_file, verifier, field):
trust=json.loads(trust_file.read_text()); trust[field]='wrong'; trust_file.write_text(json.dumps(trust))
with pytest.raises(DecisionError): checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
def test_rotation_overlap_then_removal(credential, trust_file, verifier, signature_tools):
trust=json.loads(trust_file.read_text()); trust['keys']['next-admitted']='Ivwpd5Lwtv_Av8_bftsMCqFOAlo2XsDjQuhuOCnLdLY'
trust_file.write_text(json.dumps(trust)); checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
from tests.signature_support import sign_envelope
next_body=json.dumps(sign_envelope(json.loads(FIXTURE.read_bytes()),signature_tools,next_key=True)).encode()
checked(next_body,credential,trust_file,verifier)
del trust['keys']['testdata-ed25519']; trust_file.write_text(json.dumps(trust))
with pytest.raises(DecisionError): checked(FIXTURE.read_bytes(),credential,trust_file,verifier)
checked(next_body,credential,trust_file,verifier)
def test_missing_trust_precedes_credential_read(tmp_path):
with pytest.raises(DecisionError,match='trust'):
check_decision(base_url='http://127.0.0.1:1',token_file=tmp_path/'must-not-read',request={})
def test_production_authorization_has_no_unsigned_path(monkeypatch, credential, trust_file, verifier):
observation=SimpleNamespace(request={})
monkeypatch.setattr(approval_consume,'resolve_approval_observation',lambda *a,**kw:observation)
with server(body=b'{"effect":"allow"}') as (origin,received):
cfg=SimpleNamespace(pdp_url=origin,pdp_token_file=credential,pdp_trust_file=trust_file,pdp_verifier=verifier,
authorization_policy_package='package',authorization_policy_version='v1')
with pytest.raises(DecisionError,match='authentication'):
approval_consume.authorize_action(cfg,None,'rotate')
assert received == [True]
def test_missing_trust_precedes_approval_observation(monkeypatch):
def forbidden(*a,**kw): raise AssertionError('approval credential path reached')
monkeypatch.setattr(approval_consume,'resolve_approval_observation',forbidden)
with pytest.raises(DecisionError,match='trust'):
approval_consume.authorize_action(SimpleNamespace(pdp_url='http://127.0.0.1:1',pdp_token_file=Path('/must-not-read')),None,'rotate')
@pytest.mark.parametrize('body', [b'{"effect":NaN}',b'{"effect":Infinity}',b'['*2000+b'0'+b']'*2000,b'{"x":1,"x":2}',b'\xff'])
def test_malformed_body_refuses_as_decision_error(credential,trust_file,verifier,body):
with pytest.raises(DecisionError): checked(body,credential,trust_file,verifier)
def test_default_check_requires_authentication_before_credential_read(tmp_path):
with pytest.raises(DecisionError,match='trust'):
check_decision(base_url='http://127.0.0.1:1',token_file=tmp_path/'must-not-read',request={})

View file

@ -67,7 +67,7 @@ def test_redirect_never_contacts_another_origin(credential, status):
with server() as (destination, received):
with server(status=status, location=destination + '/sink') as (source, sent):
with pytest.raises(DecisionError):
check_decision(base_url=source, token_file=credential, request={})
check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={})
assert sent == [True]
assert received == []
@ -77,18 +77,22 @@ def test_environment_proxy_never_receives_request(credential, monkeypatch):
with server() as (source, direct):
monkeypatch.setenv('http_proxy', proxy)
monkeypatch.setenv('no_proxy', '')
assert check_decision(base_url=source, token_file=credential, request={}) == {}
assert check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={}) == {}
assert direct == [True]
assert proxied == []
def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch):
from tests.signature_support import signature_tools, admit_tools
def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch, signature_tools):
observation = SimpleNamespace(request={})
monkeypatch.setattr(approval_consume, 'resolve_approval_observation', lambda *a, **kw: observation)
with server() as (destination, received):
with server(status=302, location=destination + '/sink') as (source, sent):
cfg = SimpleNamespace(pdp_url=source, pdp_token_file=credential,
authorization_policy_package='test', authorization_policy_version='v1')
admit_tools(cfg, credential.parent, signature_tools)
with pytest.raises(DecisionError):
approval_consume.authorize_action(cfg, object(), 'rotate', None)
assert sent == [True]
@ -105,7 +109,7 @@ def test_approval_flow_uses_the_same_safe_transport(credential, monkeypatch):
])
def test_invalid_address_refused_before_credential_read(url, tmp_path):
with pytest.raises(DecisionError, match='URL'):
check_decision(base_url=url, token_file=tmp_path / 'missing', request={})
check_decision(allow_unsigned_test_fixture=True, base_url=url, token_file=tmp_path / 'missing', request={})
@pytest.mark.parametrize('body,reason', [
@ -115,4 +119,4 @@ def test_invalid_address_refused_before_credential_read(url, tmp_path):
def test_response_limits_and_encoding_fail_closed(credential, body, reason):
with server(body=body) as (source, _):
with pytest.raises(DecisionError, match=reason):
check_decision(base_url=source, token_file=credential, request={})
check_decision(allow_unsigned_test_fixture=True, base_url=source, token_file=credential, request={})

View file

@ -12,7 +12,7 @@ from secrets_engine.catalog import validate_entry
from secrets_engine.errors import DecisionError
from secrets_engine.execution_contract import ExecutionContract, catalog_target_digest
from tests.test_catalog import VALID
from tests.test_consume_binding_join import _Cfg, _token, _opener, AUTH_ID
from tests.test_consume_binding_join import _Cfg, _token, _opener, APPROVAL_ID
from tests.authorization_stub import AuthorizationStub
H = 'sha256:' + 'a' * 64
@ -21,7 +21,7 @@ H = 'sha256:' + 'a' * 64
def entry():
raw = copy.deepcopy(VALID)
raw['stage'] = 'prod'
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': AUTH_ID, 'purpose': 'metadata-test'}
raw['approval'] = {'model': 'bootstrap-only', 'authorization_id': APPROVAL_ID, 'purpose': 'metadata-test'}
return validate_entry(raw)
@ -59,7 +59,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
expected = request(e, contract)
assert expected['context']['human_control'] is True
assert _expected_request(cfg,e,'execute-ccr',execution_contract=contract) == expected
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package, version=cfg.authorization_policy_version)
pdp.bind_request(expected); claim = served(pdp)
observed = resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=contract,opener=_opener(claim))
assert observed.request['context']['execution_contract'] == raw
@ -76,7 +76,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
consume_bodies = []
def consume(req, timeout=None):
value = json.loads(req.data); consume_bodies.append(value)
return _Response(200, {'status':'consumed', 'approval_id':AUTH_ID,
return _Response(200, {'status':'consumed', 'approval_id':APPROVAL_ID,
'request_digest':value['request_digest'], 'decision_id':value['decision_id'],
'consumed_at':datetime.now(timezone.utc).isoformat(), 'idempotent':False})
confirmed = consume_approval(base_url=cfg.approval_url, token_file=cfg.approval_token_file,
@ -92,7 +92,7 @@ def test_four_builders_preserve_context_and_binding(tmp_path):
@pytest.mark.parametrize('change', ['operation_id','ccr','grant','source','body','method','path','budget','deadline','state','cluster'])
def test_changed_binding_cannot_use_original_claim(tmp_path,change):
e=entry(); original=descriptor(e); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,ExecutionContract.parse(original))); claim=served(pdp)
changed=copy.deepcopy(original)
if change=='operation_id': changed[change]='775ab8f4-4731-47c8-bd98-342af22885dd'
@ -164,7 +164,7 @@ def test_expired_contract_refuses_before_claim(tmp_path):
def test_request_correspondence_changes_refuse_before_authorization(tmp_path, changed):
e = entry(); contract = ExecutionContract.parse(descriptor(e))
cfg = _Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp = AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp = AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); claim = served(pdp)
if changed == 'subject': cfg.authorization_subject_id = 'foreign-service'
elif changed == 'purpose': e = replace(e,approval={**e.approval, 'purpose':'different-purpose'})
@ -190,8 +190,13 @@ def test_raw_dictionary_and_direct_invalid_type_cannot_bypass_validation():
def test_deadline_beyond_claim_refuses_before_pdp(tmp_path):
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)+timedelta(hours=2)).isoformat()
contract=ExecutionContract.parse(raw); cfg=_Cfg(_token(tmp_path),authorization_subject_id='secrets-engine',authorization_subject_type='service')
pdp=AuthorizationStub(approval_id=AUTH_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp=AuthorizationStub(approval_id=APPROVAL_ID,package=cfg.authorization_policy_package,version=cfg.authorization_policy_version)
pdp.bind_request(request(e,contract)); calls=[]
with pytest.raises(DecisionError,match='exceeds served authority'):
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(served(pdp)),pdp_opener=lambda *a,**k:calls.append('check'))
assert not calls
# Reuse admitted native producer signing, never bypass responder verification.
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools

View file

@ -17,7 +17,7 @@ from secrets_engine.errors import CatalogError, DecisionError
from tests.authorization_stub import AuthorizationStub
from tests.test_catalog import VALID
from tests.test_consume_binding_join import (
AUTH_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
APPROVAL_ID, _Cfg, _entry, _opener, _resolve, _served, _token,
)
from tests.test_exec_owner import bound
from tests.test_privileged_cli_evidence import _config, _records
@ -98,7 +98,7 @@ def test_consume_requires_observed_human_control_even_with_demo_enabled(tmp_path
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
with pytest.raises(DecisionError, match="human_control"):
require_production_consume(_config(tmp_path), entry,
binding=ConsumeBinding(AUTH_ID, "sha256:"+"a"*64),
binding=ConsumeBinding(APPROVAL_ID, "sha256:"+"a"*64),
opener=lambda *a, **k: pytest.fail("must not consume"))
@ -108,7 +108,7 @@ def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch
monkeypatch.setenv("SECRETS_ENGINE_UNSAFE_DEMO", "1")
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
approval_token_file=_token(tmp_path))
binding = ConsumeBinding(AUTH_ID, "sha256:"+"a"*64, human_control=True)
binding = ConsumeBinding(APPROVAL_ID, "sha256:"+"a"*64, human_control=True)
calls = []
def consume(request, **kwargs):
calls.append(request.full_url)
@ -116,8 +116,8 @@ def test_declared_control_still_consumes_with_demo_enabled(tmp_path, monkeypatch
response.getcode = lambda: 200
return response
result = require_production_consume(cfg, entry, binding=binding, opener=consume)
assert result.approval_id == AUTH_ID
assert calls == [f"https://approval.example/v1/approvals/{AUTH_ID}/consume"]
assert result.approval_id == APPROVAL_ID
assert calls == [f"https://approval.example/v1/approvals/{APPROVAL_ID}/consume"]
def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
@ -127,7 +127,7 @@ def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
claim["binding"]["human_control"] = True
monkeypatch.setattr(approval_consume, "fetch_approval_claim", lambda **k: claim)
cfg = _Cfg(_token(tmp_path))
pdp = AuthorizationStub(approval_id=AUTH_ID, package=cfg.authorization_policy_package,
pdp = AuthorizationStub(approval_id=APPROVAL_ID, package=cfg.authorization_policy_package,
version=cfg.authorization_policy_version)
original_validate = approval_consume.validate_decision_envelope
def invalidate_after_check(*args, **kwargs):
@ -145,7 +145,7 @@ def test_human_declaration_is_rechecked_after_pdp(tmp_path, monkeypatch):
def test_real_exec_handler_rejects_undeclared_claim_before_consume_backend_child(bound, tmp_path, monkeypatch):
raw, _, _ = bound
raw["stage"] = "prod"
raw["approval"].update(model="decision", authorization_id=AUTH_ID,
raw["approval"].update(model="decision", authorization_id=APPROVAL_ID,
purpose="contract-test", human_control=True)
entry = validate_entry(raw)
cfg = replace(_config(tmp_path), approval_url="https://approval.example",
@ -169,3 +169,8 @@ def test_configured_factory_catalog_still_requires_human_control():
assert entry.approval["human_control"] is True
assert entry.delivery_config["exec_owner"]["status"] == "configured"
assert not entry.approval.get("authorization_id")
# Reuse admitted native producer signing, never bypass responder verification.
from tests.test_consume_binding_join import admitted_native_signer
from tests.signature_support import signature_tools

View file

@ -234,3 +234,15 @@ def test_unconfigured_engine_still_fails_closed(bao_dev, lane, tmp_path):
)
with pytest.raises(DecisionError, match="live production remains disabled"):
cli._require_lane_approval(cfg, entry, "apply")
from tests.signature_support import signature_tools, admit_tools, sign_envelope
@pytest.fixture(autouse=True)
def admitted_native_signer(monkeypatch,signature_tools):
original_cfg=_cfg
def configured(tmp_path,*args,**kwargs):
return admit_tools(original_cfg(tmp_path,*args,**kwargs),tmp_path,signature_tools)
monkeypatch.setattr(__import__(__name__,fromlist=['_cfg']),'_cfg',configured)
original_decision=AuthorizationStub.decision
monkeypatch.setattr(AuthorizationStub,'decision',lambda self,request:sign_envelope(original_decision(self,request),signature_tools))