Skip to content

cover owner revocation of policy roots - #364

Merged
samgbafa merged 4 commits into
mainfrom
skgbafa/tc-1423-n3b-owner-root-revocation-tests
Oct 19, 2026
Merged

samgbafa merged 4 commits into
mainfrom
skgbafa/tc-1423-n3b-owner-root-revocation-tests

Conversation

@samgbafa

@samgbafa samgbafa commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Linear: TC-1423 (N3b: owner revocation of policy roots, design §15, v3.3)

This PR adds tests and the client rule only; node behaviour does not change. It covers who can revoke a Policy/v3 root, over which route, and what happens after a revoke. Every revoker other than the root's own signer uses the generic POST /revoke. The JSON RootRevocation/v1 route stays limited to the root's signer and the enforcer, as today.

Changes

  • tinycloud-node-server/src/policy_v3/root_revocation_tests.rs: tests O-1 to O-7. Each runs on SQLite and on PostgreSQL through the production app (app_with_control).
  • tinycloud-node-server/src/policy_v3/test_harness.rs: a shared Policy/v3 test harness, trimmed to what these tests use. It provides did:key and did:pkh principals, owner grants, policy registration, v4 sessions, reads, decrypt, re-share, generic and JSON revocation, delegation status, status routes and hook tickets. record owner authority for policy registrations #358 has its own copy of this harness inline; once both have merged, the copy can be replaced with an import of this module.
  • docs/policy-root-revocation.md: who may revoke, on which route, the client rule, and how to confirm a revocation.
  • CHANGELOG: one bullet stating that there is no behaviour change.

Tests

test design asserts
the_space_owner_wallet_revokes_an_agent_signed_root O-1 W (did:pkh) revokes the A-signed root over generic SIWE: 200. Then the admitted session's read and the re-shared child's read get 403 root-revoked; decrypt gets the core 401 delegation-ancestor-revoked (see note); challenge and mint are refused; /delegation/status on the root shows revoked; GET /policy/v3/status gives state: revoked over an active checkpoint; the renewal POST gives 409 root-revoked. A policy session cannot hold a hook ticket, so there is no stream to close; the ticket is refused before the revoke (401 Unauthorized Action) and after it (401 delegation-ancestor-revoked). The ≤300 s hook-stream bound after a JSON-route revoke therefore does not apply to policy roots.
a_key_space_owner_revokes_over_the_generic_route_only O-2 A did:key space owner on the JSON route gets 403 root-revocation-issuer-invalid and the root stays live. The same owner over generic UCAN gets 200 and the O-1 cascade.
others_cannot_revoke_a_policy_root O-3, O-4, O-5 An unrelated key, another agent session of W, and the recipient are each refused on both routes (generic: 403 Unauthorized Revoker), and the root stays active.
a_revoke_control_naming_the_network_revokes_a_policy_root O-6 W→device control on <applications>/delegation plus the content network: generic revoke gives 200 and the cascade. The same control without the network row gets 403 Unauthorized Revoker, and the root stays live.
the_json_route_keeps_its_signer_and_enforcer_rules O-7 The enforcer is the delegatee of the enforcement root. On the generic route it gets 403 Unauthorized Revoker on both roots; this is the delegatee exclusion, and removing it fails this test on both backends. On the JSON route, the enforcer on the policy root gets 403. The enforcer on the enforcement root gets 200, with state == checkpoint.state == revoked. The signer (A) on its policy root gets 200, with state == checkpoint.state.

Finding (pre-existing, not changed here): after a generic root revoke, POST /encryption/networks/<id>/decrypt returns 401 delegation-ancestor-revoked: ancestor=<root cid> invoked=<session cid>, not 403 root-revoked. The decrypt route runs core verify_auth before the policy gate (routes/encryption.rs:151-172). The CIDs in the body are the recipient's own session and the root from its envelope. The test pins this behaviour.

Bases (the same tests at each ref)

ref result
1.21.0 (989078c) 8 passed, 2 failed (O-6 on both backends)
#353 (1338d4f) 8 passed, 2 failed (O-6 on both backends)
#356 / N4' (467093c, this PR's base) 10 passed, 0 failed

So only O-6 depends on N4'. The rest pins existing behaviour (design §15a: "already meets the ruling"). On #353 and 1.21.0, the two failures are a_revoke_control_naming_the_network_revokes_a_policy_root on sqlite and postgres.

Stack and merge order

Review delta (head 185b798)

  • F1: added the enforcer's generic-route assertion to O-7.
  • F2: generic refusals are pinned to (403, "Unauthorized Revoker").
  • F3: the hook ticket is pinned before and after the revoke.
  • F4: the harness is trimmed and #![allow(dead_code)] removed.
  • F5: the base counts are given inline above.
  • I1: the docs reference TC-1450 for the released parser's 300 s freshness limit.
  • I2: the docs and this text say the hook-stream bound is moot for policy roots.
  • Delegatee-exclusion mutation (party_may_act lets the delegatee revoke): O-7 fails on SQLite and PG, 8 passed and 2 failed.

Verification (head 185b798)

  • cargo test -p tinycloud-node --lib root_revocation_tests with PostgreSQL: 10 passed, 0 failed.
  • cargo clippy -p tinycloud-node --all-targets -- -D warnings: clean.
  • cargo fmt --all -- --check: clean.

Tests on SQLite and PostgreSQL through the production app: the space
owner's wallet (did:pkh, SIWE) and a did:key space owner revoke an
agent-signed policy root over the generic route, with the cascade to
admitted sessions, re-shared children, challenge and mint, and the status
routes; a revoke control naming the space's delegations and the
encryption network revokes it, a space-only control cannot; an unrelated
key, another agent of the wallet and the recipient are refused on both
routes; the JSON route keeps its signer and enforcer rules. Adds a shared
Policy/v3 test harness and documents the client rule.
@samgbafa

Copy link
Copy Markdown
Contributor Author

Review at head 93b3cb3 (artifacts-1423-review-n3b). Details are in the TC-1423 Linear comment.

Verified: root_revocation_tests 10/10 on SQLite and PostgreSQL (12/12 loops at 10 threads), fmt and clippy -D warnings clean, dispatched ci run at this head green; the same tests over #353 give 8/2 (only O-6 fails). Mutations on revoker_is_authorized, the space-owner branch, the control row-coverage rule, the policy-read revocation check and the JSON issuer check are all caught.

One change requested: no test covers the generic route refusing a delegatee on a policy root. Removing the delegatee exclusion in ControlAction::party_may_act leaves all 10 tests green. The enforcer is the one keyed delegatee of a root and gets 403 Unauthorized Revoker on both roots today. A short assertion in O-7 that pins this and kills the mutant:

for root in [&first.policy.policy_root_cid, &first.policy.enforcement_root_cid] {
    let (status, body) = h.post_revoke(h.enforcer.revoke(root)?).await;
    assert_eq!((status, body.as_str()), (Status::Forbidden, "Unauthorized Revoker"));
}
assert_root_live(&h, &first).await?;

Minor: the generic-refusal assertions accept 401 | 403 (the real status is 403 Unauthorized Revoker); the hook-ticket assertion is != 200 and runs only before the revoke; test_harness.rs carries unused helpers behind #![allow(dead_code)]; n3b-on-bases.txt is cited but not attached.

- The enforcer, the delegatee of the enforcement root, is refused on the
  generic route on both roots (403 Unauthorized Revoker).
- Generic refusals pin 403 Unauthorized Revoker; the hook-ticket refusal
  is pinned before (Unauthorized Action) and after the revoke
  (delegation-ancestor-revoked).
- The shared harness drops helpers no test uses, and its dead-code allow.
- Docs: policy sessions can't mint hook tickets, so the hook-stream bound
  doesn't apply; the released parser's 300-second freshness limit (TC-1450).
@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-1423
185b798

@samgbafa
samgbafa changed the base branch from tc-1423/n4-typed-revoke-scope to main October 19, 2026 19:27
@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-1423 (main-merge only; PR change unchanged vs 185b798: patch-ids and added/removed lines identical for docs/policy-root-revocation.md, policy_v3.rs, root_revocation_tests.rs, test_harness.rs; head c9bb267)

@samgbafa

Copy link
Copy Markdown
Contributor Author

APPROVE: see TC-1423 (main-merge only; PR change unchanged vs c9bb267: patch-ids and added/removed lines identical for docs/policy-root-revocation.md, policy_v3.rs, root_revocation_tests.rs, test_harness.rs; head 69d173f)

@samgbafa
samgbafa merged commit 9a2e453 into main Oct 19, 2026
22 checks passed
@samgbafa samgbafa mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant