feat(identity): prepare fixed attended registration adapter
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a140c4-354a-7823-8f0d-8f717397f786
This commit is contained in:
parent
015694ee41
commit
0775c2bfa4
4 changed files with 635 additions and 15 deletions
180
sso-mfa/k8s/keycape/keycape-registration-adapter.py
Normal file
180
sso-mfa/k8s/keycape/keycape-registration-adapter.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
"""Unexercised, injection-only attended registration seam for Warden's v1 kernel.
|
||||
|
||||
No CLI, credentials, evidence trust root or live registration is installed here.
|
||||
Owner publication/authentication and parent-session revocation are mandatory
|
||||
injected capabilities. The kernel must independently admit both evidence kinds.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from dataclasses import asdict, replace
|
||||
import json
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from warden.case_run import Adapter, Descriptor, Invocation, OwnerResult, contained_output, digest, IDENTIFIER, DIGEST
|
||||
from warden.case_receipt_state import Outcome, Refused
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
SOURCE_FILES = ('keycape-registration-adapter.py', 'keycape-client.py',
|
||||
'portal-client-rollout.py', 'openbao-client-config.py',
|
||||
'register-user-engine-portal.py')
|
||||
REFERENCE = 'net-kingdom/keycape-registration@v1'
|
||||
|
||||
# Importing the maintained helper performs no tooling operation.
|
||||
_spec = importlib.util.spec_from_file_location('registration_owner_helper', ROOT / 'keycape-client.py')
|
||||
helper = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(helper)
|
||||
|
||||
|
||||
def source_digest(check_config_digest):
|
||||
"""Pin adapter plus the complete local helper import closure, in filename order."""
|
||||
if not isinstance(check_config_digest, str) or not DIGEST.fullmatch(check_config_digest):
|
||||
raise Refused('exact reviewed checker digest required')
|
||||
composition = {'files': {name: digest((ROOT / name).read_bytes()) for name in SOURCE_FILES},
|
||||
'check_config_binary': check_config_digest}
|
||||
return digest(json.dumps(composition, sort_keys=True, separators=(',', ':')).encode())
|
||||
|
||||
|
||||
def descriptor(check_config_digest):
|
||||
return Descriptor('warden.case.execution.v1', 'register-client', 'net-kingdom',
|
||||
REFERENCE, source_digest(check_config_digest), 'human', 'key-cape-oidc-login',
|
||||
'client_id', 'change', ('keycape-client',), False,
|
||||
('parent-session',))
|
||||
|
||||
|
||||
def build(*, admitted: Invocation, case_source: bytes, expected_uid: str,
|
||||
expected_resource_version: str, case_acceptance: str,
|
||||
check_config: Path, check_config_digest: str, publish, authenticate,
|
||||
revoke_session):
|
||||
"""Bind one admitted case and observed revision; return a closed kernel Adapter.
|
||||
|
||||
publish(bytes)->identifier durably retains only the closed metadata payload.
|
||||
authenticate(identifier, bytes)->True must verify independent owner custody.
|
||||
revoke_session(invocation)->identifier must revoke this attempt's parent
|
||||
session; Warden independently authenticates it using its own evidence verifier.
|
||||
These callbacks are capabilities, never command strings or case parameters.
|
||||
|
||||
Only a verified creation in this process may be rolled back. Process loss,
|
||||
write/readback ambiguity, publication/revocation failure and source drift
|
||||
require owner reconciliation; none are converted to retry-safe failures.
|
||||
"""
|
||||
reviewed_source = source_digest(check_config_digest)
|
||||
desc = descriptor(check_config_digest)
|
||||
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 desc.adapter_source != reviewed_source:
|
||||
raise Refused('owner source changed during binding')
|
||||
if (admitted.step, admitted.owner, admitted.actor, admitted.lane) != (
|
||||
desc.step, desc.owner, desc.actor, desc.lane):
|
||||
raise Refused('registration 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_uid, expected_resource_version, case_acceptance)):
|
||||
raise Refused('bounded attended binding required')
|
||||
if digest(case_acceptance.encode()) != admitted.pins.acceptance:
|
||||
raise Refused('acceptance evidence binding differs')
|
||||
if not all(callable(x) for x in (publish, authenticate, revoke_session)):
|
||||
raise Refused('owner evidence and revocation capabilities required')
|
||||
with contained_output():
|
||||
case = yaml.safe_load(case_source)
|
||||
client = helper.client_from_case(case)
|
||||
if admitted.target != client['clientId']:
|
||||
raise Refused('case target differs')
|
||||
check_config = Path(check_config)
|
||||
records = {}
|
||||
creation = None
|
||||
|
||||
def binding(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(check_config_digest) != reviewed_source:
|
||||
raise Refused('owner source changed')
|
||||
if digest(check_config.read_bytes()) != check_config_digest:
|
||||
raise Refused('check-config binary changed')
|
||||
|
||||
def execute(inv, operation, created=()):
|
||||
nonlocal creation
|
||||
try:
|
||||
with contained_output():
|
||||
binding(inv, 'run' if operation == 'add' else 'rollback')
|
||||
uid, revision = expected_uid, expected_resource_version
|
||||
if operation == 'remove':
|
||||
if (created != ('keycape-client',) or creation is None
|
||||
or not verify(*creation)):
|
||||
raise Refused('no authenticated case-created client')
|
||||
original_payload = json.loads(records[creation[1].evidence][1])
|
||||
uid, revision = original_payload['after']['uid'], original_payload['after']['resource_version']
|
||||
args = argparse.Namespace(mode='apply', operation=operation,
|
||||
expected_uid=uid, expected_resource_version=revision,
|
||||
case_acceptance=case_acceptance, check_config=str(check_config))
|
||||
receipt = helper.run(args, client)
|
||||
# The helper has completed CAS, whole-data readback and preservation.
|
||||
# Never retain check-config diagnostics or arbitrary helper fields.
|
||||
if not (receipt['schema'] == 'netkingdom.keycape-client.v1'
|
||||
and receipt['target'] == 'sso/keycape-config'
|
||||
and receipt['client_id'] == admitted.target
|
||||
and receipt['mode'] == 'apply' and receipt['operation'] == operation
|
||||
and receipt['values_emitted'] is False
|
||||
and type(receipt['changed']) is bool
|
||||
and receipt['unrelated_config_bytes_preserved'] is True
|
||||
and receipt['other_secret_data_unchanged'] is True
|
||||
and receipt['before'] == {'uid': uid, 'resource_version': revision}
|
||||
and receipt['after']['uid'] == uid
|
||||
and receipt['check_config']['proposed']['valid'] is True):
|
||||
raise Refused('owner readback contract differs')
|
||||
after = receipt['after']
|
||||
if (set(after) != {'uid', 'resource_version'} or
|
||||
not all(isinstance(v, str) and 0 < len(v) <= 192 for v in after.values())):
|
||||
raise Refused('owner revision contract differs')
|
||||
changed = receipt['changed']
|
||||
outcome = (Outcome.ROLLED_BACK if operation == 'remove' else
|
||||
Outcome.SUCCESS if changed else Outcome.NO_CHANGE)
|
||||
objects = ('keycape-client',) if operation == 'add' and changed else ()
|
||||
finally:
|
||||
# Even helper failure must end the parent session. Failures propagate
|
||||
# contained to the kernel and become 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-registration.attempt.v1',
|
||||
'invocation': asdict(inv), 'source': reviewed_source,
|
||||
'outcome': outcome.value, 'created': objects, 'before': receipt['before'],
|
||||
'after': after, '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 operation == 'add' and changed:
|
||||
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))
|
||||
59
sso-mfa/k8s/keycape/registration-adapter.md
Normal file
59
sso-mfa/k8s/keycape/registration-adapter.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Attended registration adapter candidate
|
||||
|
||||
Exercise status: unexercised
|
||||
|
||||
This is a source-only, injection-only adapter for the existing Warden closed
|
||||
kernel. It installs no command, runtime dispatcher, credential provider, evidence
|
||||
trust root or new service. NetKingdom type/case status and bytes remain unchanged.
|
||||
The generated descriptor is a proposal; it is not owner acknowledgment.
|
||||
|
||||
`keycape-registration-adapter.py` wraps maintained `keycape-client.run` in apply
|
||||
mode. A fixed invocation binds principal, human actor, lane, case/type/descriptor/
|
||||
catalog/acceptance/run-sheet digests and target. The descriptor pins this adapter
|
||||
and its complete local helper import closure together with the exact reviewed
|
||||
`check-config` executable digest. Changing that digest changes the descriptor
|
||||
source pin and descriptor bytes, requiring independently reviewed admission.
|
||||
The fixed executable is rehashed before each attempt. The caller constructs the
|
||||
proposed invocation from exact source inputs, installs the fixed adapter once,
|
||||
and calls normal `Executor.admit`; independently derived admission must match.
|
||||
No callable replacement after admission is required or supported by this recipe.
|
||||
|
||||
The operation uses existing cluster/issuer checks, byte-preserving merge,
|
||||
Deployment secret-env prerequisite, config validation, UID/resourceVersion CAS
|
||||
and whole-data readback. Only closed metadata is passed to the injected durable
|
||||
owner publisher. The owner authenticator must independently authenticate those
|
||||
exact bytes. The session-revocation capability must end the same attempt's
|
||||
parent session; Warden separately verifies its exact revocation evidence.
|
||||
These capabilities are owner-selected callables, never case-supplied commands.
|
||||
No implementation of these native capabilities is supplied or implied here.
|
||||
|
||||
An identical existing client returns no change and creates no rollback authority.
|
||||
Removal requires this process's independently authenticated successful creation
|
||||
receipt, exact created-object label, unchanged postcreation UID/resourceVersion,
|
||||
and the original exact client record. External drift, a lost readback, receipt
|
||||
publication/revocation failure or process loss requires owner reconciliation.
|
||||
No uncertainty is labeled retry-safe. No automatic reconciliation is installed.
|
||||
Rollback after restart intentionally remains unavailable until an owner supplies
|
||||
independently authenticated durable creation provenance and reviews recovery.
|
||||
|
||||
The source tests execute actual maintained helper CAS/readback against synthetic
|
||||
Kubernetes objects. Three checks exercise actual Warden admission/run/rollback
|
||||
and uncertainty transitions with synthetic owner trust. Warden d97652a's exact
|
||||
published producer source pins apply unchanged through actual BrowserInputs.
|
||||
This is a source compatibility fixture, not acknowledgment, owner provenance,
|
||||
normal native admission or deployment acceptance.
|
||||
|
||||
Remaining work belongs in live work records before T12 closure:
|
||||
|
||||
- Warden owns global evidence admission and exact native evidence/session
|
||||
capabilities. Published d97652a refreshes BrowserInputs producer pins for
|
||||
NetKingdom 015694ee; that source assent does not attest this new adapter.
|
||||
- NetKingdom owns separately reviewed descriptor/type pin selection, the env
|
||||
binding/config/restart steps, and group ensure/revoke provenance. Current group
|
||||
ensure does not report newly added memberships; current revoke can remove
|
||||
memberships that predate the case. Wrapping these as reversible creation would
|
||||
be unsound. Preserve them until a reviewed created-only contract exists.
|
||||
- Platform owns structural/mint/broker and secret delivery evidence; Apps owns
|
||||
actual consumer login and group enforcement. No assent is supplied here.
|
||||
- Attended use still requires existing owner authorization, current caller
|
||||
identity, native source/config/image pins, native receipts and cleanup proof.
|
||||
289
sso-mfa/k8s/keycape/test_keycape_registration_adapter.py
Normal file
289
sso-mfa/k8s/keycape/test_keycape_registration_adapter.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"""Actual helper/CAS execution against synthetic Kubernetes objects only."""
|
||||
from dataclasses import asdict, replace
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from warden.case_run import Invocation, digest
|
||||
from warden.case_receipt_state import Outcome, Pins, Refused
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).parent
|
||||
spec = importlib.util.spec_from_file_location('registration_adapter', ROOT / 'keycape-registration-adapter.py')
|
||||
a = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(a)
|
||||
spec = importlib.util.spec_from_file_location('registration_fixture', ROOT / 'test_keycape_client.py')
|
||||
f = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(f)
|
||||
|
||||
|
||||
class RegistrationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.binary = Path(self.temp.name) / 'keycape'
|
||||
self.binary.write_bytes(b'synthetic checker binary')
|
||||
self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.1',
|
||||
'parameters': f.FORGEJO_PARAMS}}).encode()
|
||||
desc = a.descriptor(digest(self.binary.read_bytes()))
|
||||
self.desc_bytes = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
||||
pins = Pins(digest(self.case), digest(b'type'), digest(self.desc_bytes),
|
||||
digest(b'catalog'), digest(b'case-acceptance:test'), digest(b'run sheet'))
|
||||
self.admitted = Invocation('register-client', 'net-kingdom', 'forgejo', 'human',
|
||||
'founder', 'key-cape-oidc-login', pins)
|
||||
self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run')
|
||||
self.secret, self.raw = f.fixture()
|
||||
self.effects = []
|
||||
self.evidence = {}
|
||||
self.revocations = []
|
||||
self.kw = dict(admitted=self.admitted, case_source=self.case,
|
||||
expected_uid=f.UID, expected_resource_version='7', case_acceptance='case-acceptance:test',
|
||||
check_config=self.binary, check_config_digest=digest(self.binary.read_bytes()),
|
||||
publish=self.publish, authenticate=lambda ref, payload: self.evidence.get(ref) == payload,
|
||||
revoke_session=self.revoke)
|
||||
self.check_patch = patch.object(a.helper, 'check_config', return_value={'valid': True})
|
||||
self.kube_patch = patch.object(a.helper.lane, 'kubectl', side_effect=self.kube)
|
||||
self.check_patch.start()
|
||||
self.kube_patch.start()
|
||||
self.addCleanup(self.check_patch.stop)
|
||||
self.addCleanup(self.kube_patch.stop)
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
|
||||
def publish(self, payload):
|
||||
ref = 'owner-evidence:attempt-' + str(len(self.evidence))
|
||||
self.evidence[ref] = payload
|
||||
return ref
|
||||
|
||||
def revoke(self, inv):
|
||||
self.revocations.append(inv)
|
||||
return 'session-revoked:' + inv.operation + ':' + inv.attempt_id
|
||||
|
||||
def kube(self, args, payload=None):
|
||||
if args[:3] == ['get', 'namespace', 'kube-system']:
|
||||
return {'metadata': {'uid': a.helper.lane.CLUSTER_UID}}
|
||||
if args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']:
|
||||
return copy.deepcopy(self.secret)
|
||||
if args[:5] == ['-n', 'sso', 'get', 'deployment', 'keycape']:
|
||||
return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape',
|
||||
'env': [a.helper.env_binding(f.FORGEJO)]}]}}}}
|
||||
if args[:5] == ['-n', 'sso', 'patch', 'secret', 'keycape-config']:
|
||||
self.assertEqual(payload[:2], [
|
||||
{'op': 'test', 'path': '/metadata/uid', 'value': self.secret['metadata']['uid']},
|
||||
{'op': 'test', 'path': '/metadata/resourceVersion', 'value': self.secret['metadata']['resourceVersion']}])
|
||||
self.effects.append(copy.deepcopy(payload))
|
||||
self.secret['data']['config.yaml'] = payload[2]['value']
|
||||
self.secret['metadata']['resourceVersion'] = str(int(self.secret['metadata']['resourceVersion']) + 1)
|
||||
return copy.deepcopy(self.secret)
|
||||
raise AssertionError('unexpected tooling operation')
|
||||
|
||||
def test_actual_add_and_created_only_rollback_preserve_original_bytes(self):
|
||||
adapter = a.build(**self.kw)
|
||||
original = copy.deepcopy(self.secret['data'])
|
||||
result = adapter.invoke(self.inv)
|
||||
self.assertEqual(result.outcome, Outcome.SUCCESS)
|
||||
self.assertEqual(result.created, ('keycape-client',))
|
||||
self.assertTrue(adapter.verify(self.inv, result))
|
||||
undone = adapter.rollback(replace(self.inv, attempt_id='attempt-two', operation='rollback'), result.created)
|
||||
self.assertEqual(undone.outcome, Outcome.ROLLED_BACK)
|
||||
self.assertEqual(self.secret['data'], original)
|
||||
self.assertEqual(len(self.effects), 2)
|
||||
self.assertEqual(len(self.revocations), 2)
|
||||
for payload in self.evidence.values():
|
||||
self.assertNotIn(b'synthetic-marker', payload)
|
||||
self.assertNotIn(b'config.yaml', payload)
|
||||
self.assertNotIn(b'signing-key', payload)
|
||||
|
||||
def test_identical_client_is_no_change_and_cannot_be_removed(self):
|
||||
self.secret, _ = f.fixture(f.EXISTING + [f.FORGEJO])
|
||||
adapter = a.build(**self.kw)
|
||||
result = adapter.invoke(self.inv)
|
||||
self.assertEqual(result.outcome, Outcome.NO_CHANGE)
|
||||
self.assertEqual(result.created, ())
|
||||
with self.assertRaises(Refused):
|
||||
adapter.rollback(replace(self.inv, operation='rollback'), ('keycape-client',))
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_existing_drift_refused_and_session_revoked(self):
|
||||
self.secret, _ = f.fixture(f.EXISTING + [dict(f.FORGEJO, displayName='Drift')])
|
||||
with self.assertRaises(Refused):
|
||||
a.build(**self.kw).invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
self.assertEqual(self.revocations, [self.inv])
|
||||
|
||||
def test_invocation_rebinding_refused_before_tooling(self):
|
||||
for changes in ({'target': 'other'}, {'principal': 'other'}, {'actor': 'agent'},
|
||||
{'lane': 'other'}, {'operation': 'rollback'}, {'attempt_id': ''},
|
||||
{'pins': replace(self.inv.pins, case=digest(b'other'))}):
|
||||
with self.subTest(changes=changes), self.assertRaises(Refused):
|
||||
a.build(**self.kw).invoke(replace(self.inv, **changes))
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_initial_case_descriptor_and_target_pins_are_required(self):
|
||||
for changes in ({'case_source': b'{}'}, {'case_acceptance': 'other'}, {'admitted': replace(self.admitted, target='other')},
|
||||
{'admitted': replace(self.admitted, actor='agent')},
|
||||
{'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'wrong')))}):
|
||||
with self.subTest(changes=changes), self.assertRaises(Refused):
|
||||
a.build(**dict(self.kw, **changes))
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_binary_or_source_drift_refused_before_tooling(self):
|
||||
adapter = a.build(**self.kw)
|
||||
self.binary.write_bytes(b'changed')
|
||||
with self.assertRaises(Refused):
|
||||
adapter.invoke(self.inv)
|
||||
self.binary.write_bytes(b'synthetic checker binary')
|
||||
with patch.object(a, 'source_digest', return_value=digest(b'changed')):
|
||||
with self.assertRaises(Refused):
|
||||
adapter.invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_substituting_caller_selected_checker_under_same_descriptor_is_refused(self):
|
||||
substitute = Path(self.temp.name) / 'other-checker'
|
||||
substitute.write_bytes(b'caller-selected executable')
|
||||
with self.assertRaisesRegex(Refused, 'descriptor pin differs'):
|
||||
a.build(**dict(self.kw, check_config=substitute,
|
||||
check_config_digest=digest(substitute.read_bytes())))
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_stale_revision_refuses_mutation(self):
|
||||
adapter = a.build(**self.kw)
|
||||
self.secret['metadata']['resourceVersion'] = '8'
|
||||
with self.assertRaises(Refused):
|
||||
adapter.invoke(self.inv)
|
||||
self.assertFalse(self.effects)
|
||||
|
||||
def test_rollback_requires_exact_provenance_and_postcreation_revision(self):
|
||||
adapter = a.build(**self.kw)
|
||||
result = adapter.invoke(self.inv)
|
||||
rollback = replace(self.inv, attempt_id='attempt-two', operation='rollback')
|
||||
for created in ((), ('other',), ('keycape-client', 'other')):
|
||||
with self.subTest(created=created), self.assertRaises(Refused):
|
||||
adapter.rollback(rollback, created)
|
||||
self.secret['metadata']['resourceVersion'] = '9'
|
||||
with self.assertRaises(Refused):
|
||||
adapter.rollback(rollback, result.created)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
|
||||
def test_missing_or_tampered_evidence_never_verifies_or_authorizes_undo(self):
|
||||
adapter = a.build(**self.kw)
|
||||
result = adapter.invoke(self.inv)
|
||||
self.assertFalse(adapter.verify(replace(self.inv, attempt_id='other'), result))
|
||||
self.assertFalse(adapter.verify(self.inv, replace(result, revoked=False)))
|
||||
self.evidence[result.evidence] = b'forged'
|
||||
self.assertFalse(adapter.verify(self.inv, result))
|
||||
with self.assertRaises(Refused):
|
||||
adapter.rollback(replace(self.inv, operation='rollback'), result.created)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
|
||||
def test_process_loss_does_not_reconstruct_created_authority(self):
|
||||
result = a.build(**self.kw).invoke(self.inv)
|
||||
fresh = a.build(**self.kw)
|
||||
with self.assertRaises(Refused):
|
||||
fresh.rollback(replace(self.inv, operation='rollback'), result.created)
|
||||
self.assertIsNone(fresh.reconcile)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
|
||||
def test_failed_readback_is_uncertain_and_never_provides_creation(self):
|
||||
original_kube = self.kube
|
||||
def lost(args, payload=None):
|
||||
if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']:
|
||||
raise RuntimeError('synthetic sensitive detail')
|
||||
return original_kube(args, payload)
|
||||
with patch.object(a.helper.lane, 'kubectl', side_effect=lost):
|
||||
adapter = a.build(**self.kw)
|
||||
with self.assertRaisesRegex(Refused, '^owner callback failed$'):
|
||||
adapter.invoke(self.inv)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
self.assertFalse(self.evidence)
|
||||
self.assertEqual(self.revocations, [self.inv])
|
||||
|
||||
def test_revocation_or_publication_failure_cannot_produce_verified_success(self):
|
||||
for capability in ('publish', 'revoke_session'):
|
||||
self.secret, _ = f.fixture()
|
||||
with self.subTest(capability=capability):
|
||||
kw = dict(self.kw, **{capability: lambda *args: (_ for _ in ()).throw(RuntimeError('private'))})
|
||||
adapter = a.build(**kw)
|
||||
with self.assertRaisesRegex(Refused, '^owner callback failed$'):
|
||||
adapter.invoke(self.inv)
|
||||
self.assertFalse(self.evidence)
|
||||
|
||||
def kernel_fixture(self):
|
||||
# Fixture-only proposed type/case; canonical files are never edited.
|
||||
# Warden's published producer pins apply unchanged. Owner trust and
|
||||
# proposed descriptor/type/case are fixture-only, never native admission.
|
||||
import warden.case_inputs as inputs
|
||||
from warden.case_run import Executor, Journal
|
||||
root = ROOT.parents[2]
|
||||
case = yaml.safe_load((root / 'security-cases/forgejo-netkingdom-oidc.yaml').read_bytes())
|
||||
case['spec']['status'] = 'materialized'
|
||||
case_source = yaml.safe_dump(case).encode()
|
||||
typ = yaml.safe_load((root / 'security-case-types/oidc-browser-relying-party/type.yaml').read_bytes())
|
||||
typ['spec']['execution_descriptors'] = {'register-client': digest(self.desc_bytes)}
|
||||
type_source = yaml.safe_dump(typ).encode()
|
||||
catalog = b'fixture-only-catalog'
|
||||
sheet = json.dumps(['register-client', 'net-kingdom', 'forgejo', 'human',
|
||||
'founder', 'key-cape-oidc-login', 'fixture-acceptance', 'fixture-materialization',
|
||||
'fixture-descriptor', 'fixture-session'], separators=(',', ':')).encode()
|
||||
pins = Pins(digest(case_source), digest(type_source), digest(self.desc_bytes),
|
||||
digest(catalog), digest(b'fixture-acceptance'), digest(sheet))
|
||||
invocation = replace(self.admitted, pins=pins)
|
||||
adapter = a.build(**dict(self.kw, admitted=invocation, case_source=case_source,
|
||||
case_acceptance='fixture-acceptance'))
|
||||
def fixture_verifier(purpose, source, evidence):
|
||||
if purpose.startswith('session-revocation:'):
|
||||
_, lane, operation, attempt = purpose.split(':')
|
||||
return source == digest(case_source) and lane == invocation.lane and evidence == (
|
||||
'session-revoked:' + operation + ':' + attempt)
|
||||
return evidence in {'fixture-acceptance', 'fixture-materialization',
|
||||
'fixture-descriptor', 'fixture-session'}
|
||||
executor = Executor(Journal(Path(self.temp.name) / 'private'),
|
||||
{adapter.reference: adapter}, fixture_verifier, browser_inputs=inputs.BrowserInputs(root))
|
||||
admitted = executor.admit(case_source, type_source, self.desc_bytes, catalog,
|
||||
principal='founder', actor='human', lane=invocation.lane,
|
||||
acceptance_evidence='fixture-acceptance', materialization_evidence='fixture-materialization',
|
||||
descriptor_evidence='fixture-descriptor', session_evidence='fixture-session')
|
||||
self.assertEqual(admitted[1], invocation)
|
||||
return executor, admitted
|
||||
|
||||
def test_actual_kernel_admission_dispatch_and_rollback_with_fixture_only_trust(self):
|
||||
executor, admitted = self.kernel_fixture()
|
||||
result = executor.run(*admitted)
|
||||
self.assertEqual(result.last.outcome, Outcome.SUCCESS)
|
||||
self.assertEqual(result.last.created, ('keycape-client',))
|
||||
undone = executor.rollback(*admitted)
|
||||
self.assertEqual(undone.last.outcome, Outcome.ROLLED_BACK)
|
||||
self.assertEqual(len(self.effects), 2)
|
||||
self.assertEqual(len(self.revocations), 2)
|
||||
|
||||
def test_kernel_lost_readback_retains_uncertain_and_refuses_replay_and_undo(self):
|
||||
executor, admitted = self.kernel_fixture()
|
||||
original_kube = self.kube
|
||||
def lost(args, payload=None):
|
||||
if self.effects and args[:5] == ['-n', 'sso', 'get', 'secret', 'keycape-config']:
|
||||
raise RuntimeError('synthetic sensitive detail')
|
||||
return original_kube(args, payload)
|
||||
with patch.object(a.helper.lane, 'kubectl', side_effect=lost):
|
||||
result = executor.run(*admitted)
|
||||
self.assertEqual(result.last.outcome, Outcome.UNCERTAIN)
|
||||
self.assertFalse(result.last.created)
|
||||
with self.assertRaises(Refused):
|
||||
executor.run(*admitted, retry=True)
|
||||
with self.assertRaises(Refused):
|
||||
executor.rollback(*admitted)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
self.assertEqual(len(self.revocations), 1)
|
||||
|
||||
def test_kernel_external_revision_change_retains_uncertain_rollback(self):
|
||||
executor, admitted = self.kernel_fixture()
|
||||
self.assertEqual(executor.run(*admitted).last.outcome, Outcome.SUCCESS)
|
||||
self.secret['metadata']['resourceVersion'] = '9'
|
||||
self.assertEqual(executor.rollback(*admitted).last.outcome, Outcome.ROLLBACK_UNCERTAIN)
|
||||
self.assertEqual(len(self.effects), 1)
|
||||
self.assertEqual(len(self.revocations), 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -323,7 +323,7 @@ the rationale. The type is `acknowledged`; it becomes `implemented` with T06.
|
|||
```task
|
||||
id: NK-WP-0048-T06
|
||||
status: wait
|
||||
depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, MASON-WP-0010-T03, WARDEN-WP-0044-T03]
|
||||
depends_on: [NK-WP-0048-T05, NK-WP-0048-T08, NK-WP-0048-T09, NK-WP-0048-T12, NK-WP-0048-T13, NK-WP-0048-T14, NK-WP-0048-T15, NK-WP-0048-T16, MASON-WP-0010-T03, WARDEN-WP-0044-T03]
|
||||
blocking_reason: "Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status."
|
||||
priority: high
|
||||
state_hub_task_id: "d9b26bcf-a7c6-59a2-92a0-f913d76a203c"
|
||||
|
|
@ -592,6 +592,7 @@ prerequisites, with normal admission still refusing unimplemented types.
|
|||
id: NK-WP-0048-T10
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "4b06ca97-c1e4-5680-bf22-ab75ab1646a4"
|
||||
```
|
||||
|
||||
Completed 2026-10-16 in published source `c31d21ba`. The read-only
|
||||
|
|
@ -608,6 +609,7 @@ previously described under T06; it does not duplicate native acceptance credit.
|
|||
id: NK-WP-0048-T11
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ef2a0119-f59e-5e45-9519-115da9df7b85"
|
||||
```
|
||||
|
||||
Completed 2026-10-16. Reproduced the JSON schema rejecting a valid
|
||||
|
|
@ -626,19 +628,109 @@ publication receipt: Custodian
|
|||
id: NK-WP-0048-T12
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7ce4eee3-cd47-55a9-bfa5-4f1835d4d6fd"
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Next action: wrap the existing contained `keycape-client.py`
|
||||
registration/config/env and identity-provisioner ensure/revoke entry points in
|
||||
bounded owner adapters or receipt-only attended steps for Warden's closed kernel.
|
||||
Reuse the existing caller/session boundary, CAS/readback and status-only outputs;
|
||||
never acquire user-engine's runtime identity or expose registry/client secrets.
|
||||
Completion requires exact adapter/source descriptors, positive and refusal source
|
||||
checks, uncertainty/created-only rollback behavior and a separate owner review of
|
||||
the proposed machine seam. Source preparation can begin without credentials.
|
||||
Warden independently owns global provenance/admission integration; Platform owns
|
||||
mint/broker and native structural receipts; Apps owns consumer delivery/group
|
||||
acceptance. Their assent is not supplied by this task. Pinning descriptors into
|
||||
an acknowledged type remains a separately reviewed type-source change. Actual
|
||||
attended execution still requires its existing owner authorization and native
|
||||
receipts. T06 retains all native/reference lifecycle acceptance scope.
|
||||
Owner: NetKingdom. Retained scope: prepare bounded attended Deployment env,
|
||||
config verification and restart/readiness steps around the existing helpers.
|
||||
Next action: identify exact current entry points and their revision/readback
|
||||
contracts, then implement fixed source-pinned callbacks or receipt-only attended
|
||||
steps. Completion requires positive/refusal checks, exact target/source pins and
|
||||
uncertainty behavior without acquiring user-engine runtime identity or exposing
|
||||
registry/client secrets. Existing caller/session, cluster/issuer, CAS/readback
|
||||
and status-only output boundaries remain mandatory.
|
||||
|
||||
2026-10-16 task refinement: the original broad adapter scope is split into T13
|
||||
(completed registration source candidate), T14 (group creation provenance), T15
|
||||
(durable registration recovery) and T16 (descriptor/artifact selection and owner
|
||||
review). This preserves T12's task ID and Hub UUID while giving each remaining
|
||||
outcome an owner and completion gate. T06 now names every source dependency;
|
||||
its native/reference lifecycle acceptance scope remains unchanged. Completion
|
||||
of any source task supplies no Warden, Platform or Apps assent and no native
|
||||
execution authority.
|
||||
|
||||
## T13 — Prepare the fixed registration adapter source candidate
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T13
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Completed 2026-10-16, source implementation and synthetic fixture proof only.
|
||||
Added an injection-only adapter around actual `keycape-client.run`: fixed
|
||||
case/principal/human/lane/source/acceptance binding, existing CAS/readback,
|
||||
metadata-only owner evidence and per-attempt parent-session revocation boundary.
|
||||
The descriptor source composition pins the exact checker executable digest
|
||||
alongside adapter/helper source. Creation-only rollback authenticates its
|
||||
creation receipt and refuses changed postcreation UID/resourceVersion;
|
||||
pre-existing clients confer no rollback authority. Missing readback, evidence,
|
||||
revocation or process-local creation provenance cannot become retry-safe success.
|
||||
|
||||
Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/browser-adapter-next/`
|
||||
R3 candidate, explicit patch and postimages, retained R1 rejected custody finding
|
||||
and R2 revision, and 36 passing source checks (16 new, 20 existing). Three checks
|
||||
use actual Warden kernel admission/dispatch/rollback with published Warden
|
||||
`d97652a3bb5fa733b063d71a1b74068efcccf572` producer pins unchanged. Owner trust,
|
||||
descriptor/type/case and Kubernetes objects remain synthetic fixtures. This
|
||||
records a completed source deliverable, not independent review, publication,
|
||||
type acknowledgment, exercised procedure, identity admission or native browser
|
||||
acceptance. Independent root source review: Custodian browser-adapter-next/coordinator-review-r3.json. Source integration and normal publication/synchronization receipts: Custodian browser-adapter-next/canonical-integration.json. These receipts supply no other-owner assent or native authority.
|
||||
|
||||
## T14 — Preserve created-only provenance for group ensure and revoke
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T14
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Owner: NetKingdom. Next action: extend or wrap the existing identity-provisioner
|
||||
ensure/revoke contract with independently retained prestate and exact newly
|
||||
created groups/memberships before exposing reversible owner callbacks. Current
|
||||
ensure returns no membership-creation inventory; current revoke may remove a
|
||||
membership that predated this case. Completion requires actual helper-backed
|
||||
positive/refusal fixtures, all validation before mutation, per-attempt evidence
|
||||
and revocation, readback, ambiguity handling, and rollback that preserves every
|
||||
pre-existing/shared group and membership. No existing endpoint or service token
|
||||
is treated as new authorization.
|
||||
|
||||
## T15 — Define authenticated recovery for lost registration process state
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T15
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Owner: NetKingdom for registration state, with Warden owning kernel evidence
|
||||
admission. Next action: select the exact existing owner evidence holder/readback
|
||||
contract and define authenticated lookup for the original creation attempt.
|
||||
Completion requires restart/lost-receipt fixtures that bind case/source/principal,
|
||||
original created-only provenance and current native revision before any undo;
|
||||
uncertain outcomes remain non-replayable. The T13 adapter deliberately refuses
|
||||
post-restart rollback and installs no reconciliation callback. A new broker or
|
||||
unverified JSON receipt is not a recovery contract. Actual evidence-holder
|
||||
acceptance and attended recovery remain separately authorized native work.
|
||||
|
||||
## T16 — Select and review the actual machine descriptor composition
|
||||
|
||||
```task
|
||||
id: NK-WP-0048-T16
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Owner: NetKingdom for descriptor/type source and exact checker artifact; Warden
|
||||
independently owns global provenance/admission. Next action: identify the exact
|
||||
reviewed checker executable artifact and native owner evidence/session callback
|
||||
contracts, then assemble the complete step descriptor set and a proposed explicit
|
||||
type version selection. T13's retained descriptor pins a synthetic checker only.
|
||||
Completion requires separate owner source review of exact machine descriptors,
|
||||
artifact/source/config pins, normal type/version admission and independent
|
||||
evidence-verification boundaries. Warden d97652a refreshes producer validator
|
||||
pins for NetKingdom 015694ee; it does not attest T13's new adapter. Pinning any
|
||||
machine descriptor into an acknowledged type is a separately reviewed source
|
||||
change. Platform owns structural/mint/broker/delivery evidence; Apps owns actual
|
||||
consumer login/group enforcement. Neither their assent nor native acceptance is
|
||||
supplied by this task.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue