NK48-R2-F1: revoke session and broker grant independently in mint-secret
A raising parent-session revocation no longer skips broker-grant revocation, and the reverse; the first failure is re-raised after both are attempted, so the attempt is refused rather than reported as success. Regression tests cover each direction and both failing. 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
This commit is contained in:
parent
68f4bbb8ca
commit
eeed8e118e
3 changed files with 47 additions and 1 deletions
|
|
@ -15,6 +15,8 @@ modelled on the T13 registration adapter.
|
||||||
authenticate is destroyed, by exact version; otherwise rollback is refused.
|
authenticate is destroyed, by exact version; otherwise rollback is refused.
|
||||||
- **Revocation.** Descriptor effect `mint` with scopes `parent-session` and
|
- **Revocation.** Descriptor effect `mint` with scopes `parent-session` and
|
||||||
`broker-grant`; both are revoked after every attempt, including failures.
|
`broker-grant`; both are revoked after every attempt, including failures.
|
||||||
|
Each revocation is attempted independently: if one raises, the other is still
|
||||||
|
called, and the attempt is refused rather than reported as success.
|
||||||
- Confidential clients only; case, descriptor, acceptance and attempt pins bound
|
- Confidential clients only; case, descriptor, acceptance and attempt pins bound
|
||||||
as in T13. Not retry-safe.
|
as in T13. Not retry-safe.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,21 @@ def build(*, admitted: Invocation, case_source: bytes, case_acceptance: str,
|
||||||
receipt = destroy(path, version)
|
receipt = destroy(path, version)
|
||||||
check(receipt, 'destroy', version)
|
check(receipt, 'destroy', version)
|
||||||
finally:
|
finally:
|
||||||
|
# Each revocation is attempted independently: a failing session
|
||||||
|
# revocation must not leave the broker grant alive, or the reverse.
|
||||||
|
# The first failure is re-raised only after both were attempted.
|
||||||
with contained_output():
|
with contained_output():
|
||||||
session, grant = revoke_session(inv), revoke_grant(inv)
|
session = grant = failure = None
|
||||||
|
try:
|
||||||
|
session = revoke_session(inv)
|
||||||
|
except BaseException as exc:
|
||||||
|
failure = exc
|
||||||
|
try:
|
||||||
|
grant = revoke_grant(inv)
|
||||||
|
except BaseException as exc:
|
||||||
|
failure = failure or exc
|
||||||
|
if failure is not None:
|
||||||
|
raise failure
|
||||||
for ref in (session, grant):
|
for ref in (session, grant):
|
||||||
if not isinstance(ref, str) or not IDENTIFIER.fullmatch(ref):
|
if not isinstance(ref, str) or not IDENTIFIER.fullmatch(ref):
|
||||||
raise Refused('revocation evidence missing')
|
raise Refused('revocation evidence missing')
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,37 @@ class MintSecretTests(unittest.TestCase):
|
||||||
self.assertEqual([c[0] for c in self.calls], [])
|
self.assertEqual([c[0] for c in self.calls], [])
|
||||||
self.assertEqual(self.revoked, [('session', 'rollback'), ('grant', 'rollback')])
|
self.assertEqual(self.revoked, [('session', 'rollback'), ('grant', 'rollback')])
|
||||||
|
|
||||||
|
def _failing(self, kind):
|
||||||
|
def revoke(inv):
|
||||||
|
self.revoked.append((kind, inv.operation))
|
||||||
|
raise RuntimeError(kind + ' revocation unavailable')
|
||||||
|
return revoke
|
||||||
|
|
||||||
|
def test_failing_session_revocation_still_revokes_grant(self):
|
||||||
|
self.kw['revoke_session'] = self._failing('session')
|
||||||
|
adapter = a.build(**self.kw)
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
adapter.invoke(self.run)
|
||||||
|
self.assertEqual(self.revoked, [('session', 'run'), ('grant', 'run')])
|
||||||
|
self.assertEqual(self.evidence, {}) # never published as success
|
||||||
|
|
||||||
|
def test_failing_grant_revocation_still_revokes_session(self):
|
||||||
|
self.kw['revoke_grant'] = self._failing('grant')
|
||||||
|
adapter = a.build(**self.kw)
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
adapter.invoke(self.run)
|
||||||
|
self.assertEqual(self.revoked, [('session', 'run'), ('grant', 'run')])
|
||||||
|
self.assertEqual(self.evidence, {})
|
||||||
|
|
||||||
|
def test_both_revocations_failing_attempts_both_and_refuses(self):
|
||||||
|
self.kw['revoke_session'] = self._failing('session')
|
||||||
|
self.kw['revoke_grant'] = self._failing('grant')
|
||||||
|
adapter = a.build(**self.kw)
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
adapter.rollback(self.back, a.CREATED)
|
||||||
|
self.assertEqual(self.revoked, [('session', 'rollback'), ('grant', 'rollback')])
|
||||||
|
self.assertEqual(self.evidence, {})
|
||||||
|
|
||||||
def test_receipt_with_value_or_extra_field_refused(self):
|
def test_receipt_with_value_or_extra_field_refused(self):
|
||||||
for bad in (receipt('mint', value='x'), receipt('mint', values_emitted=True),
|
for bad in (receipt('mint', value='x'), receipt('mint', values_emitted=True),
|
||||||
receipt('mint', path='platform/workloads/keycape/other'),
|
receipt('mint', path='platform/workloads/keycape/other'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue