177 lines
7.2 KiB
Python
177 lines
7.2 KiB
Python
|
|
"""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
|