Bind typed execution context across authorization requests
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
e24437607b
commit
d8edd9e516
6 changed files with 475 additions and 4 deletions
|
|
@ -47,8 +47,10 @@ for the first proof; none is selected or approved by this design.
|
|||
|
||||
## Exact request binding
|
||||
|
||||
Use the existing `authorization.build_action_request(entry, 'apply', ...)` shape
|
||||
and canonical digest rules. Add a reviewed `context.execution_contract` object
|
||||
Use `authorization.build_action_request(entry, 'execute-ccr', ...)` with the
|
||||
existing canonical digest rules. `apply` is a distinct legacy action: its v2
|
||||
policy ignores this metadata contract and must never stand in for execute-ccr.
|
||||
Add a reviewed `context.execution_contract` object
|
||||
before observing the claim or calling Check. Its fields are:
|
||||
|
||||
| Field | Binding |
|
||||
|
|
@ -246,3 +248,66 @@ failure/recovery rules and native counterexamples. No success in the initial
|
|||
metadata pilot unblocks these rows by default. Full T06 adoption requires the
|
||||
actual selected engine production workflow to run through the corresponding
|
||||
reviewed entitlement, with native effect, denial/no-fallback and cleanup proof.
|
||||
|
||||
## Source preparation: typed request propagation (2026-10-11)
|
||||
|
||||
The staged ExecutionContract descriptor is deliberately a closed, immutable
|
||||
non-secret value. `parse` validates shape and captures JSON; every `as_dict`
|
||||
revalidates even directly constructed instances and returns a detached copy.
|
||||
It is not an owner approval, source authenticity attestation, principal
|
||||
representation or proof that the requested origin is admitted. Compiler and
|
||||
mediator must independently reconstruct and authenticate sources and bodies.
|
||||
|
||||
The exact proposed descriptor serialization refines the table above:
|
||||
|
||||
- Digests use `sha256:` plus 64 lowercase hexadecimal characters. UUIDs use
|
||||
canonical lowercase hyphenated form. CCR and grant IDs must be distinct.
|
||||
- `source_bundle` has exactly engine_dispatcher, platform_mediator, typed_ccr and
|
||||
policy_auth; each has exactly an absolute normalized `path` and `sha256`.
|
||||
This parses paths without reading files; pin verification remains unimplemented.
|
||||
- `operations` is an ordered list of exactly method/path/payload_sha256 objects.
|
||||
It accepts only GET/PUT metadata policy and auth-role endpoint shapes, with
|
||||
normalized relative paths. This schema does not reconstruct bodies or prove
|
||||
that all before/write/readback operations occur or target exactly two paths.
|
||||
A later owner compiler must enforce those semantics before any execution.
|
||||
- `backend_origin` is an explicit-port HTTP(S) origin without credentials, path,
|
||||
query or fragment. Syntactic acceptance creates no approved route or network
|
||||
call. Estate pinning and server authentication remain mediator obligations.
|
||||
- `backend_budget` has exactly ttl_seconds (integer 30–120) and uses (integer
|
||||
1–12); booleans fail. Listed calls must leave at least one cleanup use.
|
||||
Renewal, remint and live per-call accounting remain outside this descriptor.
|
||||
- expected_before/expected_after are non-secret state digests; operation_deadline
|
||||
is timezone-aware. Before claim, refuse expired deadlines using the existing
|
||||
trusted-time window. After claim and Check, refuse a deadline later than either
|
||||
served approval or decision expiry. A future mediator must recheck before every
|
||||
effect and provide separately authorized cleanup after business expiry.
|
||||
- catalog_sha256 is a descriptor-local sorted compact JSON fingerprint of the
|
||||
complete existing context.catalog_target, distinct from canonical PDP digests.
|
||||
Resource catalog ID/stage, subject, tenant and purpose remain bound separately
|
||||
through the unchanged canonical request hashing implementation.
|
||||
|
||||
The same typed descriptor passes through build_action_request, _expected_request,
|
||||
resolve_approval_observation and authorize_action. Any contract on apply or another
|
||||
legacy action refuses. execute-ccr without a typed valid contract, prod stage or
|
||||
platform tenant refuses. execute-ccr always binds human_control=true, even on a
|
||||
legacy bootstrap catalog, and cannot use an unserved/demo fallback. Invalid or
|
||||
catalog-mismatched descriptors refuse before claim/PDP/CAS. Returned requests are
|
||||
independent copies; later caller mutation cannot change the stored descriptor.
|
||||
|
||||
99 focused source tests pass across context propagation, the existing approval
|
||||
join, decision validation, readiness, consume and engine auth. New tests exercise
|
||||
all four actual builders, actual consume request formatting and claim-free/full
|
||||
joins; mutation of operation, CCR/grant/source/body/method/path/budget/deadline/
|
||||
estate/state/principal/purpose/catalog/tenant/stage refuses the original approval.
|
||||
Transport/PDP stand-ins deliberately supply hypothetical human control and an
|
||||
ALLOW: they prove propagation, not a shipped policy or real principal admission.
|
||||
|
||||
No execute-ccr CLI, identity-owner verified initiating-principal interface,
|
||||
new access policy package, typed owner compiler, task-grant materializer,
|
||||
independent authenticated consume-confirmation integration, durable admission
|
||||
journal, restricted backend, per-call deadline checks or native entitlement is
|
||||
supplied by this stage. Existing cfg subject strings remain legacy provenance;
|
||||
this change does not make them verified human representation. Existing generic
|
||||
consume receipt validation limitations remain explicitly separate. service-jwt
|
||||
readiness guard/default and ordinary actions remain unchanged. T01 stays in
|
||||
progress, T03 stays wait, WP13 stays proposed, and WP8-T06 stays open.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from urllib.error import HTTPError, URLError
|
|||
from urllib.request import Request, urlopen
|
||||
|
||||
from secrets_engine.approval_auth import approval_auth_configured, approval_token, credential_urlopen
|
||||
from secrets_engine.execution_contract import ExecutionContract, require_execution_deadline
|
||||
from secrets_engine.approval_claim import observe_pdp_approval_claim, validate_approval_claim
|
||||
from secrets_engine.catalog import human_control_required
|
||||
from secrets_engine.decision_check import check_decision
|
||||
|
|
@ -148,6 +149,7 @@ def _expected_request(
|
|||
fields: tuple[str, ...] = (),
|
||||
policy_targets: tuple[str, ...] = (),
|
||||
auth_targets: tuple[str, ...] = (),
|
||||
execution_contract: ExecutionContract | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the exact CheckRequest both steps must agree on.
|
||||
|
||||
|
|
@ -175,6 +177,7 @@ def _expected_request(
|
|||
fields=fields,
|
||||
policy_targets=policy_targets,
|
||||
auth_targets=auth_targets,
|
||||
execution_contract=execution_contract,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -230,6 +233,7 @@ def resolve_approval_observation(
|
|||
fields: tuple[str, ...] = (),
|
||||
policy_targets: tuple[str, ...] = (),
|
||||
auth_targets: tuple[str, ...] = (),
|
||||
execution_contract: ExecutionContract | None = None,
|
||||
opener: Callable[..., Any] | None = None,
|
||||
) -> ApprovalObservation | None:
|
||||
"""Observe a fresh fact and prepare the claim-bearing request (step 1).
|
||||
|
|
@ -238,20 +242,30 @@ def resolve_approval_observation(
|
|||
human control raises instead, so no stage/demo fallback can discharge it.
|
||||
Correspondence and a consume binding require authorize_action's PDP step.
|
||||
"""
|
||||
execution_request = None
|
||||
if action == "execute-ccr" or execution_contract is not None:
|
||||
execution_request = _expected_request(
|
||||
cfg, entry, action, fields=fields, policy_targets=policy_targets,
|
||||
auth_targets=auth_targets, execution_contract=execution_contract,
|
||||
)
|
||||
# The caller's descriptor is not authenticated principal/owner evidence.
|
||||
# This requirement cannot disappear on an unserved/demo lane.
|
||||
base_url = str(getattr(cfg, "approval_url", "") or "")
|
||||
auth_configured = approval_auth_configured(cfg)
|
||||
authorization_id = _authorization_id(entry, decision)
|
||||
required_human_control = human_control_required(entry)
|
||||
required_human_control = action == "execute-ccr" or human_control_required(entry)
|
||||
if not base_url or not auth_configured or not authorization_id:
|
||||
if required_human_control:
|
||||
raise DecisionError("human_control requires a served approval claim and exact action binding")
|
||||
return None
|
||||
|
||||
expected_request = _expected_request(
|
||||
expected_request = execution_request or _expected_request(
|
||||
cfg, entry, action,
|
||||
fields=fields, policy_targets=policy_targets, auth_targets=auth_targets,
|
||||
execution_contract=execution_contract,
|
||||
)
|
||||
|
||||
require_execution_deadline(expected_request, time_window=read_window(cfg))
|
||||
claim = fetch_approval_claim(
|
||||
base_url=base_url,
|
||||
token_provider=lambda: approval_token(cfg, scope="approval:read"),
|
||||
|
|
@ -260,6 +274,8 @@ def resolve_approval_observation(
|
|||
)
|
||||
observe_pdp_approval_claim(claim, approval_id=authorization_id,
|
||||
require_human_control=required_human_control, time_window=read_window(cfg))
|
||||
require_execution_deadline(expected_request, time_window=read_window(cfg),
|
||||
bounds=(claim["validity"]["expires_at"],))
|
||||
expected_request["context"]["approval"] = claim
|
||||
return ApprovalObservation(authorization_id, expected_request, claim)
|
||||
|
||||
|
|
@ -273,6 +289,7 @@ def authorize_action(
|
|||
fields: tuple[str, ...] = (),
|
||||
policy_targets: tuple[str, ...] = (),
|
||||
auth_targets: tuple[str, ...] = (),
|
||||
execution_contract: ExecutionContract | None = None,
|
||||
opener: Callable[..., Any] | None = None,
|
||||
pdp_opener: Callable[..., Any] | None = None,
|
||||
) -> AuthorizedAction | None:
|
||||
|
|
@ -288,6 +305,7 @@ def authorize_action(
|
|||
fields=fields,
|
||||
policy_targets=policy_targets,
|
||||
auth_targets=auth_targets,
|
||||
execution_contract=execution_contract,
|
||||
opener=opener,
|
||||
)
|
||||
if observation is None:
|
||||
|
|
@ -334,6 +352,8 @@ def authorize_action(
|
|||
require_human_control=expected_request["context"].get("human_control") is True,
|
||||
time_window=window,
|
||||
)
|
||||
require_execution_deadline(expected_request, time_window=window,
|
||||
bounds=(observation.claim["validity"]["expires_at"], validated.expires_at))
|
||||
if validated.action != action:
|
||||
raise DecisionError("access-engine decision does not bind this action")
|
||||
return AuthorizedAction(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from typing import Any
|
|||
|
||||
from secrets_engine.catalog import CatalogEntry, human_control_required
|
||||
from secrets_engine.errors import DecisionError
|
||||
from secrets_engine.execution_contract import ExecutionContract, attach_execution_contract
|
||||
from secrets_engine.application_time import validity_bounds
|
||||
|
||||
DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
|
|
@ -67,6 +68,7 @@ def build_action_request(
|
|||
auth_targets: list[str] | tuple[str, ...] = (),
|
||||
request_id: str = "",
|
||||
tenant: str = REQUEST_TENANT,
|
||||
execution_contract: ExecutionContract | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the exact normalized secrets-engine profile for flex-auth.
|
||||
|
||||
|
|
@ -121,6 +123,7 @@ def build_action_request(
|
|||
},
|
||||
}
|
||||
)
|
||||
attach_execution_contract(request, execution_contract)
|
||||
if human_control_required(entry):
|
||||
# Bind the requested control to the exact PDP submission. This is a
|
||||
# requirement on the observed approval, not an authorization verdict.
|
||||
|
|
|
|||
176
src/secrets_engine/execution_contract.py
Normal file
176
src/secrets_engine/execution_contract.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Closed, immutable metadata descriptor; shape is never execution authority.
|
||||
|
||||
This source-only compiler input cannot grant an action, authenticate a principal,
|
||||
prove owner source authenticity or open a backend. Those integration gates remain
|
||||
outside this type. Existing actions cannot borrow the new descriptor.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import PurePosixPath
|
||||
from urllib.parse import urlsplit
|
||||
from uuid import UUID
|
||||
|
||||
from secrets_engine.errors import DecisionError
|
||||
|
||||
VERSION = "SECRETS-EXEC-20261011-v1"
|
||||
HASH = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||
KEYS = {
|
||||
"contract_version", "operation_id", "operation", "execution_actor", "ccr_id",
|
||||
"ccr_immutable_sha256", "execution_grant_ccr_id", "grant_immutable_sha256",
|
||||
"catalog_sha256", "source_bundle", "backend_origin", "cluster_uid",
|
||||
"operations", "operation_deadline", "backend_budget", "expected_before",
|
||||
"expected_after",
|
||||
}
|
||||
SOURCE_ROLES = {"engine_dispatcher", "platform_mediator", "typed_ccr", "policy_auth"}
|
||||
|
||||
|
||||
def _refuse() -> None:
|
||||
# Do not interpolate untrusted descriptors into messages/evidence.
|
||||
raise DecisionError("invalid execution_contract descriptor")
|
||||
|
||||
|
||||
def _closed(value, keys):
|
||||
if type(value) is not dict or set(value) != set(keys):
|
||||
_refuse()
|
||||
return value
|
||||
|
||||
|
||||
def _text(value):
|
||||
if type(value) is not str or not value or len(value) > 4096 or any(ord(c) < 32 for c in value):
|
||||
_refuse()
|
||||
return value
|
||||
|
||||
|
||||
def _hash(value):
|
||||
if type(value) is not str or not HASH.fullmatch(value):
|
||||
_refuse()
|
||||
|
||||
|
||||
def _path(value, *, absolute=False):
|
||||
_text(value)
|
||||
if (value.startswith("/") != absolute or "\\" in value or "%" in value
|
||||
or "?" in value or "#" in value or ":" in value
|
||||
or any(p in {"", ".", ".."} for p in value.lstrip("/").split("/"))
|
||||
or str(PurePosixPath(value)) != value):
|
||||
_refuse()
|
||||
|
||||
|
||||
def _validated(value):
|
||||
value = copy.deepcopy(_closed(value, KEYS))
|
||||
if value['contract_version'] != VERSION or value['operation'] != 'metadata-apply-ccr' or value['execution_actor'] != 'service:secrets-engine':
|
||||
_refuse()
|
||||
try:
|
||||
if str(UUID(_text(value['operation_id']))) != value['operation_id']:
|
||||
_refuse()
|
||||
except (ValueError, AttributeError):
|
||||
_refuse()
|
||||
for key in ['ccr_id', 'execution_grant_ccr_id', 'cluster_uid']:
|
||||
_text(value[key])
|
||||
if value['ccr_id'] == value['execution_grant_ccr_id']:
|
||||
_refuse()
|
||||
for key in ['ccr_immutable_sha256', 'grant_immutable_sha256', 'catalog_sha256', 'expected_before', 'expected_after']:
|
||||
_hash(value[key])
|
||||
sources = _closed(value['source_bundle'], SOURCE_ROLES)
|
||||
for source in sources.values():
|
||||
_closed(source, {'path', 'sha256'})
|
||||
_path(source['path'], absolute=True)
|
||||
_hash(source['sha256'])
|
||||
try:
|
||||
origin = urlsplit(_text(value['backend_origin']))
|
||||
if (origin.scheme not in {'http', 'https'} or not origin.hostname
|
||||
or origin.username is not None or origin.password is not None
|
||||
or origin.path or origin.query or origin.fragment or origin.port is None):
|
||||
_refuse()
|
||||
except ValueError:
|
||||
_refuse()
|
||||
try:
|
||||
deadline = datetime.fromisoformat(_text(value['operation_deadline']).replace('Z', '+00:00'))
|
||||
if deadline.tzinfo is None:
|
||||
_refuse()
|
||||
except ValueError:
|
||||
_refuse()
|
||||
budget = _closed(value['backend_budget'], {'ttl_seconds', 'uses'})
|
||||
if (type(budget['ttl_seconds']) is not int or not 30 <= budget['ttl_seconds'] <= 120
|
||||
or type(budget['uses']) is not int or not 1 <= budget['uses'] <= 12):
|
||||
_refuse()
|
||||
operations = value['operations']
|
||||
if type(operations) is not list or not 1 <= len(operations) < budget['uses']:
|
||||
_refuse() # Reserve at least one cleanup use; no silent retries.
|
||||
for operation in operations:
|
||||
_closed(operation, {'method', 'path', 'payload_sha256'})
|
||||
if type(operation['method']) is not str or operation['method'] not in {'GET', 'PUT'}:
|
||||
_refuse()
|
||||
_path(operation['path'])
|
||||
# Only metadata pilot endpoint shapes, never KV/mount/token/secretid.
|
||||
if not re.fullmatch(r'(sys/policies/acl/[A-Za-z0-9_-]+|auth/[A-Za-z0-9_-]+/role/[A-Za-z0-9_-]+)', operation['path']):
|
||||
_refuse()
|
||||
_hash(operation['payload_sha256'])
|
||||
return value
|
||||
|
||||
|
||||
def catalog_target_digest(target: dict) -> str:
|
||||
"""Descriptor-local sorted JSON fingerprint, distinct from PDP digest."""
|
||||
encoded = json.dumps(target, sort_keys=True, ensure_ascii=False, allow_nan=False,
|
||||
separators=(',', ':')).encode('utf-8')
|
||||
return 'sha256:' + hashlib.sha256(encoded).hexdigest()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecutionContract:
|
||||
_serialized: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, descriptor: dict) -> 'ExecutionContract':
|
||||
validated = _validated(descriptor)
|
||||
return cls(json.dumps(validated, sort_keys=True, separators=(',', ':'), allow_nan=False))
|
||||
|
||||
def as_dict(self) -> dict:
|
||||
# Revalidate even a directly constructed instance: no trusted label bypass.
|
||||
try:
|
||||
return _validated(json.loads(self._serialized))
|
||||
except (ValueError, TypeError, RecursionError):
|
||||
_refuse()
|
||||
|
||||
|
||||
def attach_execution_contract(request: dict, contract: ExecutionContract | None) -> None:
|
||||
action = request['action']
|
||||
if action != 'execute-ccr':
|
||||
if contract is not None:
|
||||
raise DecisionError('execution_contract requires execute-ccr action')
|
||||
return
|
||||
if type(contract) is not ExecutionContract:
|
||||
raise DecisionError('execute-ccr requires typed execution_contract')
|
||||
value = contract.as_dict()
|
||||
if request['resource']['attributes']['stage'] != 'prod':
|
||||
raise DecisionError('execute-ccr requires prod stage')
|
||||
if request['tenant'] != 'tenant:platform':
|
||||
raise DecisionError('execute-ccr requires platform tenant')
|
||||
if value['catalog_sha256'] != catalog_target_digest(request['context']['catalog_target']):
|
||||
raise DecisionError('execution_contract catalog target does not match')
|
||||
request['context']['execution_contract'] = value
|
||||
request['context']['human_control'] = True
|
||||
|
||||
|
||||
def require_execution_deadline(request: dict, *, time_window=None, bounds=()) -> None:
|
||||
"""Check trusted-time freshness and served upper bounds before CAS binding."""
|
||||
contract = request.get('context', {}).get('execution_contract')
|
||||
if contract is None:
|
||||
return
|
||||
from secrets_engine.application_time import validity_bounds
|
||||
deadline = datetime.fromisoformat(contract['operation_deadline'].replace('Z', '+00:00'))
|
||||
_, upper = validity_bounds(None, time_window)
|
||||
if upper >= deadline:
|
||||
raise DecisionError('execution_contract deadline has expired')
|
||||
for bound in bounds:
|
||||
try:
|
||||
limit = datetime.fromisoformat(bound.replace('Z', '+00:00'))
|
||||
if limit.tzinfo is None or deadline > limit:
|
||||
raise DecisionError('execution_contract deadline exceeds served authority')
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
raise DecisionError('execution_contract requires served deadline') from None
|
||||
197
tests/test_execution_context.py
Normal file
197
tests/test_execution_context.py
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
"""Source-only builder tests; mocks confer no real grant or principal admission."""
|
||||
import copy
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from secrets_engine.approval_consume import consume_approval, _expected_request, authorize_action, resolve_approval_observation
|
||||
from secrets_engine.authorization import approval_binding_digest, build_action_request, request_digest
|
||||
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.authorization_stub import AuthorizationStub
|
||||
|
||||
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'}
|
||||
return validate_entry(raw)
|
||||
|
||||
|
||||
def descriptor(e):
|
||||
baseline = build_action_request(e, 'apply', subject_id='secrets-engine', subject_type='service', purpose='metadata-test')
|
||||
return {
|
||||
'contract_version': 'SECRETS-EXEC-20261011-v1', 'operation_id': 'cbfbced0-09d5-48c8-a1e7-a2ea4dc69153',
|
||||
'operation': 'metadata-apply-ccr', 'execution_actor': 'service:secrets-engine',
|
||||
'ccr_id': 'CCR-2099-0001', 'ccr_immutable_sha256': H, 'execution_grant_ccr_id': 'CCR-2099-0002',
|
||||
'grant_immutable_sha256': H, 'catalog_sha256': catalog_target_digest(baseline['context']['catalog_target']),
|
||||
'source_bundle': {name: {'path': '/owner/'+name+'.py', 'sha256': H}
|
||||
for name in ['engine_dispatcher', 'platform_mediator', 'typed_ccr', 'policy_auth']},
|
||||
'backend_origin': 'http://127.0.0.1:18200', 'cluster_uid': 'test-estate-not-live',
|
||||
'operations': [{'method': 'GET', 'path': 'sys/policies/acl/test-policy', 'payload_sha256': H},
|
||||
{'method': 'PUT', 'path': 'auth/netkingdom/role/test-role', 'payload_sha256': H}],
|
||||
'operation_deadline': (datetime.now(timezone.utc)+timedelta(seconds=60)).isoformat(),
|
||||
'backend_budget': {'ttl_seconds': 120, 'uses': 12}, 'expected_before': H, 'expected_after': H,
|
||||
}
|
||||
|
||||
|
||||
def request(e, contract):
|
||||
return build_action_request(e, 'execute-ccr', subject_id='secrets-engine', subject_type='service',
|
||||
purpose='metadata-test', execution_contract=contract)
|
||||
|
||||
|
||||
def served(pdp):
|
||||
claim = pdp.claim()
|
||||
claim['binding']['human_control'] = True
|
||||
return claim
|
||||
|
||||
|
||||
def test_four_builders_preserve_context_and_binding(tmp_path):
|
||||
e = entry(); raw = descriptor(e); contract = ExecutionContract.parse(raw)
|
||||
cfg = _Cfg(_token(tmp_path), authorization_subject_id='secrets-engine', authorization_subject_type='service')
|
||||
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.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
|
||||
assert approval_binding_digest(observed.request) == request_digest(expected)
|
||||
assert request_digest(observed.request) != request_digest(expected)
|
||||
submitted=[]
|
||||
def check(req, timeout=None):
|
||||
value=json.loads(req.data); submitted.append(value)
|
||||
return _opener(pdp.decision(value))(req)
|
||||
authorized = authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
|
||||
assert authorized.binding.request_digest == request_digest(submitted[0])
|
||||
assert submitted[0] == observed.request
|
||||
from tests.test_approval_consume import _Response
|
||||
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,
|
||||
'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,
|
||||
binding=authorized.binding, opener=consume)
|
||||
assert consume_bodies == [{'request_digest':request_digest(submitted[0]), 'decision_id':authorized.decision_id}]
|
||||
assert confirmed.request_digest != request_digest(expected)
|
||||
raw['operations'][0]['path']='sys/policies/acl/changed'
|
||||
observed.request['context']['execution_contract']['source_bundle']['typed_ccr']['path']='/changed.py'
|
||||
assert contract.as_dict()['operations'][0]['path']=='sys/policies/acl/test-policy'
|
||||
assert contract.as_dict()['source_bundle']['typed_ccr']['path']=='/owner/typed_ccr.py'
|
||||
|
||||
|
||||
@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.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'
|
||||
elif change=='ccr': changed['ccr_immutable_sha256']='sha256:'+'b'*64
|
||||
elif change=='grant': changed['grant_immutable_sha256']='sha256:'+'b'*64
|
||||
elif change=='source': changed['source_bundle']['typed_ccr']['sha256']='sha256:'+'b'*64
|
||||
elif change=='body': changed['operations'][1]['payload_sha256']='sha256:'+'b'*64
|
||||
elif change=='method': changed['operations'][0]['method']='PUT'
|
||||
elif change=='path': changed['operations'][0]['path']='sys/policies/acl/changed'
|
||||
elif change=='budget': changed['backend_budget']['uses']=11
|
||||
elif change=='deadline': changed['operation_deadline']=(datetime.now(timezone.utc)+timedelta(seconds=50)).isoformat()
|
||||
elif change=='state': changed['expected_before']='sha256:'+'b'*64
|
||||
elif change=='cluster': changed['cluster_uid']='different-estate'
|
||||
typed=ExecutionContract.parse(changed)
|
||||
assert approval_binding_digest(request(e,typed)) != pdp._pdp_digest
|
||||
def check(req,timeout=None): return _opener(pdp.decision(json.loads(req.data)))(req)
|
||||
with pytest.raises(DecisionError,match='digest'):
|
||||
authorize_action(cfg,e,'execute-ccr',execution_contract=typed,opener=_opener(claim),pdp_opener=check)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mutation', ['unknown','extra_nested','missing','wrong_actor','boolean_budget','oversize_budget','sibling_endpoint','path_traversal','source_relative','wrong_hash','bad_deadline','method_object'])
|
||||
def test_closed_schema_refuses(mutation):
|
||||
raw=descriptor(entry())
|
||||
if mutation=='unknown': raw['approved']=True
|
||||
elif mutation=='extra_nested': raw['backend_budget']['renewable']=True
|
||||
elif mutation=='missing': del raw['source_bundle']['typed_ccr']
|
||||
elif mutation=='wrong_actor': raw['execution_actor']='user:admin'
|
||||
elif mutation=='boolean_budget': raw['backend_budget']['uses']=True
|
||||
elif mutation=='oversize_budget': raw['backend_budget']['ttl_seconds']=121
|
||||
elif mutation=='sibling_endpoint': raw['operations'][0]['path']='secret/data/x'
|
||||
elif mutation=='path_traversal': raw['operations'][0]['path']='auth/a/role/../x'
|
||||
elif mutation=='source_relative': raw['source_bundle']['typed_ccr']['path']='foo.py'
|
||||
elif mutation=='wrong_hash': raw['expected_after']='unsafe'
|
||||
elif mutation=='bad_deadline': raw['operation_deadline']='2026-10-11T10:00:00'
|
||||
elif mutation=='method_object': raw['operations'][0]['method']={}
|
||||
with pytest.raises(DecisionError): ExecutionContract.parse(raw)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('action', ['apply','rotate','exec','execute-ccr'])
|
||||
def test_invalid_action_or_absent_contract_refuses_before_claim(tmp_path,action):
|
||||
e=entry(); cfg=_Cfg(_token(tmp_path)); calls=[]
|
||||
def fetch(*args,**kwargs): calls.append('claim'); raise AssertionError
|
||||
contract=None if action=='execute-ccr' else ExecutionContract.parse(descriptor(e))
|
||||
with pytest.raises(DecisionError):
|
||||
authorize_action(cfg,e,action,execution_contract=contract,opener=fetch,pdp_opener=fetch)
|
||||
assert calls==[]
|
||||
|
||||
|
||||
def test_catalog_mismatch_and_unserved_execution_refuse(tmp_path):
|
||||
e=entry(); typed=ExecutionContract.parse(descriptor(e)); e=replace(e,path='changed/path')
|
||||
cfg=_Cfg(_token(tmp_path)); calls=[]
|
||||
with pytest.raises(DecisionError,match='catalog'):
|
||||
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=typed,opener=lambda *a,**k:calls.append('claim'))
|
||||
assert not calls
|
||||
e=entry(); cfg.approval_url=''
|
||||
with pytest.raises(DecisionError,match='human_control'):
|
||||
resolve_approval_observation(cfg,e,'execute-ccr',None,execution_contract=ExecutionContract.parse(descriptor(e)))
|
||||
|
||||
|
||||
def test_expired_contract_refuses_before_claim(tmp_path):
|
||||
e=entry(); raw=descriptor(e); raw['operation_deadline']=(datetime.now(timezone.utc)-timedelta(seconds=1)).isoformat()
|
||||
cfg=_Cfg(_token(tmp_path)); calls=[]
|
||||
with pytest.raises(DecisionError,match='expired'):
|
||||
authorize_action(cfg,e,'execute-ccr',execution_contract=ExecutionContract.parse(raw),opener=lambda *a,**k:calls.append('claim'))
|
||||
assert not calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize('changed', ['subject', 'purpose', 'catalog', 'tenant', 'stage'])
|
||||
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.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'})
|
||||
elif changed == 'catalog': e = replace(e,path='changed/path')
|
||||
elif changed == 'stage': e = replace(e,stage='test')
|
||||
calls = []
|
||||
def check(req,timeout=None):
|
||||
calls.append('check'); return _opener(pdp.decision(json.loads(req.data)))(req)
|
||||
with pytest.raises(DecisionError):
|
||||
if changed == 'tenant':
|
||||
build_action_request(e,'execute-ccr',subject_id='secrets-engine',subject_type='service',purpose='metadata-test',tenant='tenant:foreign',execution_contract=contract)
|
||||
else:
|
||||
authorize_action(cfg,e,'execute-ccr',execution_contract=contract,opener=_opener(claim),pdp_opener=check)
|
||||
if changed in {'catalog','stage','tenant'}: assert not calls
|
||||
|
||||
|
||||
def test_raw_dictionary_and_direct_invalid_type_cannot_bypass_validation():
|
||||
e = entry()
|
||||
with pytest.raises(DecisionError): request(e,descriptor(e))
|
||||
with pytest.raises(DecisionError): request(e,ExecutionContract('{}'))
|
||||
|
||||
|
||||
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.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
|
||||
|
|
@ -35,6 +35,16 @@ owner-held ephemeral two-path token, bounded TTL/uses, durable admission and
|
|||
cleanup. Independent Platform/Secrets Engine/identity/approval/PDP owner returns
|
||||
are required. No foreign review is written by this draft.
|
||||
|
||||
2026-10-11 source-only preparation: typed execution_contract is staged through
|
||||
all four request builders. The new descriptor is restricted to execute-ccr,
|
||||
requires prod/platform/human control and binds the catalog fingerprint before
|
||||
claim. Existing apply/other actions cannot borrow this descriptor. 99 focused
|
||||
source checks verify propagation, canonical approval/full consume joins,
|
||||
immutable copies and changed-binding refusals. Detailed schema/limitations are
|
||||
in the execution-contract document. No source owner assent, initiating-principal
|
||||
admission, new policy package, ledger/mediator/backend or native acceptance is
|
||||
claimed. Independent source review remains required. Task statuses are unchanged.
|
||||
|
||||
## Prevent consumption without execution readiness
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue