Lane key-cape-oidc-login -> keycape-attended-case-change in the register-client, ensure-groups and bind-env descriptors; deliver-consumer-secret actor agent -> human; verify_consumer reports a value-free, exactly-once probe record list bound into the observation digest. Composition artifacts, type.yaml and admit/compose tests are left for the orchestrator to regenerate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 903097@bnt-lap001 Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
221 lines
11 KiB
Python
221 lines
11 KiB
Python
"""Unexercised, injection-only bind-env seam for Warden's v1 kernel (NK-WP-0048-T27).
|
|
|
|
Given an admitted oidc-browser-relying-party case (canonical 1.1 by default via the
|
|
maintained helper), add exactly one `env` entry to the `keycape` container of the
|
|
issuer Deployment: name from the helper, valueFrom.secretKeyRef to the delivered
|
|
secret *reference*. No CLI, kubectl, credential, or value handling lives here;
|
|
read/write are injected capabilities with compare-and-swap on resourceVersion.
|
|
Only an entry this process created, authenticated and still byte-identical, may
|
|
be rolled back. Owner publication/authentication and parent-session revocation
|
|
are mandatory injected capabilities; no evidence trust root is installed here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from dataclasses import asdict, replace
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER
|
|
from warden.case_receipt_state import Outcome, Refused
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
REFERENCE = 'net-kingdom/keycape-bind-env@v1'
|
|
# keycape-client.py loads portal-client-rollout.py at import, which loads
|
|
# openbao-client-config.py and register-user-engine-portal.py; all of them run.
|
|
SOURCE_FILES = ('bind-env-adapter.py', 'keycape-client.py',
|
|
'portal-client-rollout.py', 'openbao-client-config.py',
|
|
'register-user-engine-portal.py')
|
|
CREATED = 'keycape-env-binding'
|
|
_spec = importlib.util.spec_from_file_location('bind_env_owner_helper', ROOT / 'keycape-client.py')
|
|
helper = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(helper)
|
|
|
|
|
|
def _container(deployment):
|
|
if (deployment.get('kind') != 'Deployment'
|
|
or deployment.get('metadata', {}).get('name') != 'keycape'
|
|
or deployment.get('metadata', {}).get('namespace') != 'sso'):
|
|
raise Refused('issuer deployment sso/keycape required')
|
|
found = [c for c in deployment['spec']['template']['spec'].get('containers', [])
|
|
if c.get('name') == 'keycape']
|
|
if len(found) != 1:
|
|
raise Refused('exactly one keycape container required')
|
|
return found[0]
|
|
|
|
|
|
def binding_for_case(case, *, allow_unimplemented=False):
|
|
if allow_unimplemented:
|
|
raise Refused('allow-unimplemented admission is not supported')
|
|
client = helper.client_from_case(case)
|
|
if client.get('clientType') != 'confidential':
|
|
raise Refused('public client has no secret reference to bind')
|
|
return helper.env_binding(client)
|
|
|
|
|
|
def _check_reference_only(entry):
|
|
if 'value' in entry or set(entry) != {'name', 'valueFrom'} or set(entry['valueFrom']) != {'secretKeyRef'}:
|
|
raise Refused('reference-only env binding required')
|
|
|
|
|
|
def plan(deployment, binding):
|
|
"""Return (new_deployment, created). Never reads or emits a secret value."""
|
|
_check_reference_only(binding)
|
|
new = copy.deepcopy(deployment)
|
|
env = _container(new).setdefault('env', [])
|
|
same = [e for e in env if e.get('name') == binding['name']]
|
|
if len(same) > 1:
|
|
raise Refused('duplicate env name present')
|
|
if same:
|
|
if same[0] != binding:
|
|
raise Refused('env name already bound to a different source')
|
|
return new, False
|
|
env.append(copy.deepcopy(binding))
|
|
return new, True
|
|
|
|
|
|
def source_digest():
|
|
"""Pin adapter plus the complete local helper import closure, in filename order."""
|
|
composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES}}
|
|
return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode())
|
|
|
|
|
|
def descriptor():
|
|
return Descriptor('warden.case.execution.v1', 'bind-env', 'net-kingdom', REFERENCE,
|
|
source_digest(), 'human', 'keycape-attended-case-change', 'client_id', 'change',
|
|
(CREATED,), False, ('parent-session',))
|
|
|
|
|
|
def build(*, admitted: Invocation, case_source: bytes, expected_resource_version: str,
|
|
case_acceptance: str, read, write, publish, authenticate, revoke_session,
|
|
allow_unimplemented=False):
|
|
"""Bind one admitted case and observed revision; return a closed kernel Adapter.
|
|
|
|
read()->deployment dict; write(deployment, expected_rv)->new rv (CAS, raises
|
|
on conflict). publish(bytes)->identifier retains only the closed metadata
|
|
payload; authenticate(identifier, bytes)->True verifies owner custody;
|
|
revoke_session(invocation)->identifier revokes this attempt's parent session.
|
|
Process loss, readback ambiguity and publication/revocation failure require
|
|
owner reconciliation; none are converted to retry-safe failures.
|
|
"""
|
|
reviewed_source = source_digest()
|
|
desc = descriptor()
|
|
if type(admitted) is not Invocation or not all(
|
|
isinstance(value, str) and IDENTIFIER.fullmatch(value)
|
|
for value in (admitted.step, admitted.owner, admitted.target,
|
|
admitted.actor, admitted.principal, admitted.lane)):
|
|
raise Refused('bounded invocation required')
|
|
if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != (
|
|
desc.step, desc.owner, desc.actor, desc.lane):
|
|
raise Refused('bind-env invocation boundary differs')
|
|
if admitted.operation != 'admission' or admitted.attempt_id:
|
|
raise Refused('expected admission invocation')
|
|
if type(case_source) is not bytes or len(case_source) > 262144 or digest(case_source) != admitted.pins.case:
|
|
raise Refused('case source pin differs')
|
|
# Pins.adapter is the descriptor digest, not the owner source digest.
|
|
descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
|
if admitted.pins.adapter != digest(descriptor_bytes):
|
|
raise Refused('descriptor pin differs')
|
|
if not all(isinstance(x, str) and 0 < len(x) <= 192
|
|
for x in (expected_resource_version, case_acceptance)):
|
|
raise Refused('observed resourceVersion and acceptance required')
|
|
if digest(case_acceptance.encode()) != admitted.pins.acceptance:
|
|
raise Refused('acceptance evidence binding differs')
|
|
if not all(callable(x) for x in (read, write, publish, authenticate, revoke_session)):
|
|
raise Refused('read/write, owner evidence and revocation capabilities required')
|
|
with contained_output():
|
|
case = yaml.safe_load(case_source)
|
|
client = helper.client_from_case(case)
|
|
binding = binding_for_case(case, allow_unimplemented=allow_unimplemented)
|
|
if admitted.target != client['clientId']:
|
|
raise Refused('case target differs')
|
|
records = {}
|
|
creation = None
|
|
|
|
def bound(inv, operation):
|
|
if (type(inv) is not Invocation
|
|
or replace(inv, attempt_id='', operation='admission') != admitted
|
|
or inv.operation != operation or not IDENTIFIER.fullmatch(inv.attempt_id)):
|
|
raise Refused('attempt binding differs')
|
|
if source_digest() != reviewed_source:
|
|
raise Refused('owner source changed')
|
|
|
|
def add():
|
|
current = read()
|
|
before = current['metadata'].get('resourceVersion')
|
|
if before != expected_resource_version:
|
|
raise Refused('deployment revision differs from observed')
|
|
new, created = plan(current, binding)
|
|
if not created:
|
|
return Outcome.NO_CHANGE, (), before, before
|
|
after = write(new, before)
|
|
back = read()
|
|
if (not isinstance(after, str) or not 0 < len(after) <= 192
|
|
or back['metadata'].get('resourceVersion') != after or plan(back, binding)[1]):
|
|
raise Refused('readback ambiguous; owner reconciliation required')
|
|
return Outcome.SUCCESS, (CREATED,), before, after
|
|
|
|
def remove(created):
|
|
if created != (CREATED,) or creation is None or not verify(*creation):
|
|
raise Refused('no authenticated case-created binding')
|
|
created_rv = json.loads(records[creation[1].evidence][1])['after']
|
|
current = read()
|
|
before = current['metadata'].get('resourceVersion')
|
|
if before != created_rv:
|
|
raise Refused('deployment changed since creation; owner reconciliation required')
|
|
new = copy.deepcopy(current)
|
|
env = _container(new).get('env', [])
|
|
matches = [i for i, e in enumerate(env) if e == binding]
|
|
if len(matches) != 1 or [e for e in env if e.get('name') == binding['name']] != [binding]:
|
|
raise Refused('created binding not found unchanged')
|
|
del env[matches[0]]
|
|
after = write(new, before)
|
|
back = read()
|
|
if (not isinstance(after, str) or back['metadata'].get('resourceVersion') != after
|
|
or any(e.get('name') == binding['name'] for e in _container(back).get('env', []))):
|
|
raise Refused('rollback readback ambiguous; owner reconciliation required')
|
|
return Outcome.ROLLED_BACK, (), before, after
|
|
|
|
def execute(inv, operation, created=()):
|
|
nonlocal creation
|
|
try:
|
|
with contained_output():
|
|
bound(inv, 'run' if operation == 'add' else 'rollback')
|
|
outcome, objects, before, after = add() if operation == 'add' else remove(created)
|
|
finally:
|
|
# Even failure must end the parent session; failures propagate to the
|
|
# kernel as uncertain, never safe to replay.
|
|
with contained_output():
|
|
revocation = revoke_session(inv)
|
|
if not isinstance(revocation, str) or not IDENTIFIER.fullmatch(revocation):
|
|
raise Refused('session revocation evidence missing')
|
|
payload = json.dumps({'schema': 'netkingdom.keycape-bind-env.attempt.v1',
|
|
'invocation': asdict(inv), 'source': reviewed_source, 'outcome': outcome.value,
|
|
'created': objects, 'env_name': binding['name'], 'before': before, 'after': after,
|
|
'values_emitted': False, 'session_revocation': revocation},
|
|
sort_keys=True, separators=(',', ':')).encode()
|
|
with contained_output():
|
|
evidence = publish(payload)
|
|
if not isinstance(evidence, str) or not IDENTIFIER.fullmatch(evidence) or evidence in records:
|
|
raise Refused('owner evidence identifier invalid or reused')
|
|
result = OwnerResult(outcome, evidence, objects, True, revocation,
|
|
attempt_id=inv.attempt_id, operation=inv.operation)
|
|
records[evidence] = (inv, payload, result)
|
|
if objects:
|
|
creation = (inv, result)
|
|
elif operation == 'remove':
|
|
creation = None
|
|
return result
|
|
|
|
def verify(inv, result):
|
|
record = records.get(result.evidence) if type(result) is OwnerResult else None
|
|
if record is None or record[0] != inv or record[2] != result:
|
|
return False
|
|
with contained_output():
|
|
return authenticate(result.evidence, record[1]) is True
|
|
|
|
return Adapter(REFERENCE, reviewed_source, lambda inv: execute(inv, 'add'),
|
|
verify, lambda inv, created: execute(inv, 'remove', created))
|