Bind typed execution context across authorization requests
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.1-sol
Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
tegwick 2026-10-11 16:28:54 +02:00
parent e24437607b
commit d8edd9e516
6 changed files with 475 additions and 4 deletions

View file

@ -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(

View file

@ -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.

View 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