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
217 lines
12 KiB
Python
217 lines
12 KiB
Python
"""Unexercised, injection-only ensure-groups seam for Warden's v1 kernel (T24).
|
|
|
|
Source candidate only. No CLI, credentials, LLDAP session, SQL connection or
|
|
evidence trust root is installed here. Group creation is bound to T14's
|
|
journal/provenance seam: the injected provisioner must carry a
|
|
PostgresGroupProvenance whose GroupJournalBinding names this exact case,
|
|
principal and reviewed adapter source, with independent continuity callbacks.
|
|
SQLite/local journals, a missing journal or a differing binding are refused.
|
|
Owner source review and every native action remain separate gates.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, replace
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
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
|
|
PROVISIONER = ROOT.parents[2] / 'identity-provisioner'
|
|
SOURCE_FILES = (('sso-mfa/k8s/keycape', 'ensure-groups-adapter.py'),
|
|
('identity-provisioner', 'provisioner.py'),
|
|
('identity-provisioner', 'group_provenance.py'),
|
|
('identity-provisioner', 'group_provenance_postgres.py'))
|
|
REFERENCE = 'net-kingdom/ensure-groups@v1'
|
|
CASE_TYPE = 'oidc-browser-relying-party@1.1' # canonical 1.1 stays the default
|
|
# 1.2 and 1.3 are reachable only by explicit version-root selection, never by default.
|
|
# Its ensure-groups step and access_groups/initial_members parameters are
|
|
# unchanged from 1.1, so the same request parsing applies to both.
|
|
SELECTABLE_TYPES = {'1.1': CASE_TYPE, '1.2': 'oidc-browser-relying-party@1.2',
|
|
'1.3': 'oidc-browser-relying-party@1.3'}
|
|
CREATED = ('service-group', 'service-group-membership')
|
|
|
|
if str(PROVISIONER) not in sys.path:
|
|
sys.path.insert(0, str(PROVISIONER))
|
|
from group_provenance_postgres import PostgresGroupProvenance # noqa: E402 (no I/O on import)
|
|
|
|
|
|
def source_digest():
|
|
"""Pin adapter plus the provisioner/journal import closure, in path order."""
|
|
base = ROOT.parents[2]
|
|
files = {f'{d}/{n}': digest((base / d / n).read_bytes()) for d, n in SOURCE_FILES}
|
|
return digest(json.dumps({'files': files}, sort_keys=True, separators=(',', ':')).encode())
|
|
|
|
|
|
def descriptor():
|
|
return Descriptor('warden.case.execution.v1', 'ensure-groups', 'net-kingdom', REFERENCE,
|
|
source_digest(), 'human', 'keycape-attended-case-change', 'client_id', 'change',
|
|
CREATED, False, ('parent-session',))
|
|
|
|
|
|
def group_binding(groups, members, correlation_id) -> str:
|
|
"""Public form of the provisioner's exact ensure binding (journal key binding).
|
|
|
|
Must stay byte-identical to identity-provisioner's Provisioner._group_binding;
|
|
the test suite pins that equality so drift fails closed in review.
|
|
"""
|
|
canonical = {'ensure_correlation_id': correlation_id, 'groups': sorted(groups),
|
|
'members': {g: sorted(members.get(g, [])) for g in sorted(groups)}}
|
|
return hashlib.sha256(json.dumps(canonical, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
|
|
|
|
|
def _request(case, version_root='1.1'):
|
|
spec = case.get('spec') if isinstance(case, dict) else None
|
|
expected = SELECTABLE_TYPES.get(version_root) if isinstance(version_root, str) else None
|
|
if expected is None or not isinstance(spec, dict) or spec.get('type') != expected:
|
|
raise Refused('case type differs from selected version root')
|
|
params = spec.get('parameters')
|
|
if not isinstance(params, dict) or not isinstance(params.get('access_groups'), str):
|
|
raise Refused('access_groups required')
|
|
groups = params['access_groups'].split(',')
|
|
if len(set(groups)) != len(groups):
|
|
raise Refused('duplicate access group')
|
|
members = {}
|
|
raw = params.get('initial_members', '')
|
|
if not isinstance(raw, str):
|
|
raise Refused('initial_members must be a string')
|
|
for entry in filter(None, raw.split(';')):
|
|
group, _, users = entry.partition('=')
|
|
if group not in groups or group in members or not users:
|
|
raise Refused('initial_members names a group outside access_groups')
|
|
members[group] = users.split('+')
|
|
return params.get('client_id'), groups, members
|
|
|
|
|
|
def build(*, admitted: Invocation, case_source: bytes, provisioner, publish, authenticate,
|
|
revoke_session, delete_created_if_empty: bool = False, version_root: str = '1.1'):
|
|
"""Bind one admitted case to the owner journal; return a closed kernel Adapter.
|
|
|
|
provisioner exposes ensure_service_groups/revoke_service_groups and carries
|
|
the trusted owner journal as group_provenance. publish(bytes)->identifier and
|
|
authenticate(identifier, bytes)->True are owner evidence capabilities.
|
|
revoke_session(invocation)->identifier revokes this attempt's parent session;
|
|
it runs in a finally block, so even a failed attempt ends the session.
|
|
|
|
The idempotency key and correlation derive from the case pin, so every
|
|
attempt resumes the one journal receipt and never recreates removed state.
|
|
version_root selects the accepted case type: '1.1' (default, canonical) or
|
|
'1.2' / '1.3' only when passed explicitly. Unknown roots are refused.
|
|
|
|
Rollback reverses only journal-confirmed creations; preexisting/shared
|
|
groups and memberships are preserved. Ambiguity propagates as uncertain.
|
|
"""
|
|
reviewed = source_digest()
|
|
desc = descriptor()
|
|
if type(admitted) is not Invocation or not all(
|
|
isinstance(v, str) and IDENTIFIER.fullmatch(v)
|
|
for v 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('ensure-groups 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')
|
|
descriptor_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
|
if admitted.pins.adapter != digest(descriptor_bytes):
|
|
raise Refused('descriptor pin differs')
|
|
journal = getattr(provisioner, 'group_provenance', None)
|
|
if type(journal) is not PostgresGroupProvenance:
|
|
raise Refused('trusted owner journal callbacks required')
|
|
binding = journal.binding
|
|
if (binding.case_digest, binding.source_digest, binding.principal) != (
|
|
admitted.pins.case, reviewed, admitted.principal):
|
|
raise Refused('owner journal binding differs from this case')
|
|
if not all(callable(getattr(provisioner, m, None)) for m in ('ensure_service_groups', 'revoke_service_groups')):
|
|
raise Refused('owner group operations required')
|
|
if not all(callable(x) for x in (publish, authenticate, revoke_session)) or type(delete_created_if_empty) is not bool:
|
|
raise Refused('owner evidence capabilities required')
|
|
if version_root not in SELECTABLE_TYPES:
|
|
raise Refused('unknown version root')
|
|
with contained_output():
|
|
client_id, groups, members = _request(yaml.safe_load(case_source), version_root)
|
|
if client_id != admitted.target:
|
|
raise Refused('case target differs')
|
|
key = 'warden-ensure:' + admitted.pins.case
|
|
request = {'groups': groups, 'members': members, 'correlation_id': admitted.pins.case}
|
|
journal_binding = group_binding(groups, members, admitted.pins.case)
|
|
records = {}
|
|
|
|
def check_attempt(inv, operation):
|
|
if (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:
|
|
raise Refused('owner source changed')
|
|
|
|
def execute(inv, rollback, created=()):
|
|
try:
|
|
with contained_output():
|
|
check_attempt(inv, 'rollback' if rollback else 'run')
|
|
if rollback:
|
|
if not created or set(created) - set(CREATED):
|
|
raise Refused('only journal-confirmed creations may be rolled back')
|
|
result = provisioner.revoke_service_groups(dict(
|
|
request, idempotency_key=key + ':revoke', ensure_idempotency_key=key,
|
|
ensure_correlation_id=admitted.pins.case, delete_if_empty=delete_created_if_empty))
|
|
expected = {'groups_revoked', 'groups_revoked_and_deleted'}
|
|
else:
|
|
result = provisioner.ensure_service_groups(dict(request, idempotency_key=key))
|
|
expected = {'groups_ensured'}
|
|
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')
|
|
if (result.status not in expected or result.ensure_idempotency_key != key
|
|
or result.ensure_correlation_id != admitted.pins.case
|
|
or result.external_subject != ','.join(sorted(groups))):
|
|
raise Refused('owner journal result contract differs')
|
|
objects = tuple(name for name, items in zip(CREATED, (result.created_groups, result.created_memberships)) if items)
|
|
if rollback:
|
|
outcome = Outcome.ROLLED_BACK
|
|
elif result.resumed or not objects:
|
|
outcome, objects = Outcome.NO_CHANGE, ()
|
|
else:
|
|
outcome = Outcome.SUCCESS
|
|
# Names and ids only: the journal holds no secret values.
|
|
payload = json.dumps({'schema': 'netkingdom.ensure-groups.attempt.v1',
|
|
'invocation': asdict(inv), 'source': reviewed, 'outcome': outcome.value,
|
|
'created': objects, 'journal_key': key, 'status': result.status,
|
|
'created_groups': list(result.created_groups),
|
|
'created_memberships': list(result.created_memberships),
|
|
'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')
|
|
owner = OwnerResult(outcome, evidence, objects, True, revocation,
|
|
attempt_id=inv.attempt_id, operation=inv.operation)
|
|
records[evidence] = (inv, payload, owner)
|
|
return owner
|
|
|
|
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():
|
|
try:
|
|
state = journal.get(key, journal_binding)['state']
|
|
except Exception:
|
|
return False
|
|
want = 'revoked' if result.outcome is Outcome.ROLLED_BACK else 'ensured'
|
|
return state == want and authenticate(result.evidence, record[1]) is True
|
|
|
|
return Adapter(REFERENCE, reviewed, lambda inv: execute(inv, False), verify,
|
|
lambda inv, created: execute(inv, True, created))
|