[New Permission 5/5] smartcontract: enforce topology/resource/index permission flags#3943
Conversation
ReviewThis swaps the direct The fixed-layout processors ( 1.
|
- Add execute_authorized_transaction_quiet to DoubleZeroClient trait and DZClient impl, restoring quiet mode for ban and closeaccount commands that was lost when switching to execute_authorized_transaction - Restore onchain allocation support in CreateSubscribeUserCommand SDK command (feature-flag-gated ResourceExtension PDA logic removed in permission enforcement refactor) - Restore atomic path tests and fixture resource extension PDA fields in create_subscribe_user_test.rs
- SDK: route execute_transaction and execute_authorized_transaction through a single execute_transaction_inner, restoring the protocol-max compute-budget / heap-frame requests, skip_preflight, and structured error handling that the build_and_send detour had dropped for the whole SDK (C1/H1). - SDK: append the Permission PDA as the trailing account so it stays after payer+system, matching authorize()'s read order; the quiet authorized variant now shares the same builder (H2/H3/L3). - SDK: memoize the Permission PDA lookup and retry it on transient RPC errors, removing the per-send un-retried get_account (M3). - SDK: add DZClient-level tests asserting compute-budget presence and the trailing Permission account, with and without a permission account (M2). - authorize: drop the retired activator authority from USER_ADMIN/NETWORK_ADMIN/ MULTICAST_ADMIN legacy fallbacks; document the ACCESS_PASS_ADMIN expansion (M1). - authorize: verify program ownership before inspecting account data (L1). - accesspass/set: document the optional-account count invariant (M4). - CHANGELOG: correct the SDK entry to match the unified builder.
…delete/ban DeleteUserCommand and RequestBanUserCommand authorize the final instruction with USER_ADMIN, but first strip the user's multicast roles through UpdateMulticastGroupRoles, whose processor only accepted the access-pass owner or a foundation member. A USER_ADMIN-only payer was therefore blocked on the prerequisite cleanup. Allow the role-update processor to accept USER_ADMIN for the removal-only case (no roles being granted), and route UpdateMulticastGroupRolesCommand through execute_authorized_transaction so the payer's Permission account is appended when present. Adding roles still requires the owner or foundation.
…to-injection The SDK auto-appends the payer's Permission PDA whenever it exists on-chain, so a foundation member whose own Permission account was suspended, under-privileged, or uninitialized was routed through the Some branch of authorize() and denied the PERMISSION_ADMIN instruction needed to repair it — the None-branch lockout fallback never ran. Extract foundation_permission_recovery() and apply it in the Some branch too: when the supplied Permission account does not grant the flag, a foundation member requesting PERMISSION_ADMIN is still allowed. PDA-address and ownership checks remain hard errors; recovery is scoped to PERMISSION_ADMIN only.
…rmission flags Add TOPOLOGY_ADMIN (1<<15), RESOURCE_ADMIN (1<<16), and INDEX_ADMIN (1<<17) to permission_flags, with legacy authorization mapping each to the foundation allowlist in authorize() (plus unit tests). Expose the new names in the serviceability CLI (topology-admin/resource-admin/index-admin) and add the matching constants to the Go, TypeScript, and Python SDKs. Documents the flags in PERMISSION.md and the serviceability README. This defines the permissions; enforcement in the processors lands in 4/4.
696cfc3 to
0c9e787
Compare
…ermission flags Replace the direct foundation_allowlist checks in the topology (create/delete/clear/assign-node-segments), resource (create/allocate/deallocate/close), and index (create/delete) instructions with authorize() calls requesting TOPOLOGY_ADMIN/RESOURCE_ADMIN/INDEX_ADMIN. The Permission account is read as the optional trailing account; the variable-length clear and assign-node-segments layouts detect it by PDA match before consuming their link/device lists. Backward compatible: with no Permission account the legacy foundation path still applies. Switch the corresponding SDK commands to execute_authorized_transaction so the payer's Permission PDA is appended when it exists, and add an end-to-end test covering topology creation via a TOPOLOGY_ADMIN Permission account.
1a8d0db to
929958f
Compare
… accounts The clear processor read payer/system_program at fixed front positions [2]/[3] while the SDK client appends them (plus the optional Permission account) after the variable-length link list, so doublezero topology clear reverted whenever links were passed. Parse payer/system/permission off the tail instead, mirroring assign_node_segments, and drop the redundant payer from the SDK fixed_accounts. Add a production-layout test helper and a TOPOLOGY_ADMIN permission clear test so the real on-wire layout is exercised (the previous helper placed payer/system before the extras, masking the mismatch).
|
Thanks for the thorough review, both findings are valid and I've pushed a fix in 77e86d4. #1 #2 test helper masked it. Confirmed.
|
…ation in existing instructions (#3206) > **Permission rollout — stacked PR series.** Review/merge order: > 1. [#3206 — enforce Permission-based authorization in existing instructions](#3206) > 2. [#3942 — define topology/resource/index permission flags (3/4)](#3942) — stacked on #3206 > 3. [#3943 — enforce topology/resource/index permission flags (4/4)](#3943) — stacked on #3942 > > Retarget each PR's base to `main` as its upstream merges. > **👉 You are here: #3206 (base of the follow-up stack).** ## Summary of Changes - Wire `authorize()` into the privileged instruction processors that gate on it: `accesspass/{close,set}` (`ACCESS_PASS_ADMIN`) and `user/{delete,requestban}` (`USER_ADMIN`). Each appends the caller's Permission PDA as an optional trailing account, read last by `authorize()`. The user owner can still delete their own account without a Permission account. - Add `execute_authorized_transaction` (and its `_quiet` variant) to the Rust SDK. They share the existing `execute_transaction_inner` builder, so compute-budget, preflight, and error-reporting behavior are identical to `execute_transaction`; the only difference is the optional trailing Permission PDA, which is resolved at most once per client (retried on transient RPC errors, then memoized). - Switch the affected SDK commands (`accesspass/{close,set}`, `user/{delete,requestban}`, `tenant/delete`, and the `permission/*` CRUD commands) to `execute_authorized_transaction` so the Permission account is included transparently. - Drop the retired activator authority from the `USER_ADMIN`/`NETWORK_ADMIN`/`MULTICAST_ADMIN` legacy fallbacks. `ACCESS_PASS_ADMIN` legacy authority (foundation + sentinel + feed) is applied uniformly across its instructions and documented in `authorize.rs`. ## Testing Verification - New DZClient-level tests assert the assembled instruction list carries the protocol-max compute-budget/heap-frame requests and that the Permission account is appended after `payer`+`system`, with and without an on-chain permission account. - `authorize` unit tests cover the legacy fallbacks (including the activator now being denied for the admin roles) and the Permission-account path (PDA, ownership, discriminator, status, and flag checks). - `cargo test -p doublezero_sdk` and `cargo test -p doublezero-serviceability` pass. Related RFC/series: the Permission account and `authorize()` mechanism land in the earlier PRs of this series; this PR enforces them in existing instructions.
|
🚫 Blocking (verified):
Mismatch
Why the existing tests don't catch it: the integration tests build accounts via Verified by feeding the exact SDK ordering to the real processor through the banks-client harness: Contrast with Suggested fix: align The rest of the PR looks good — the seven fixed-layout processors all consume |
There was a problem hiding this comment.
Approving to unblock the stack. One blocking-quality issue noted in a separate comment: topology clear reverts via the SDK/CLI due to an account-ordering mismatch in ClearTopologyCommand (verified against the real processor). It looks pre-existing (RFC-18 PR 2/5) rather than introduced here, so approving — but please address it (here or a fast follow-up) since this PR migrates clear to the new authorization path. Everything else looks correct.
UPDATE: I think this was basically Ben's comment and you fixed it.
…rmission flags (#3942) > **Permission rollout — stacked PR series.** Review/merge order: > 1. [#3206 — enforce Permission-based authorization in existing instructions](#3206) > 2. [#3942 — define topology/resource/index permission flags (3/4)](#3942) — stacked on #3206 > 3. [#3943 — enforce topology/resource/index permission flags (4/4)](#3943) — stacked on #3942 > > Retarget each PR's base to `main` as its upstream merges. > **👉 You are here: #3942 (PR 3/4).** ## Summary - Adds three permission flags — `TOPOLOGY_ADMIN` (`1<<15`), `RESOURCE_ADMIN` (`1<<16`), `INDEX_ADMIN` (`1<<17`) — so segment-routing topologies, `ResourceExtension` accounts, and internal `Index` accounts can be delegated without granting the broad `FOUNDATION` flag (today they are foundation-only). - Maps each flag to the `foundation_allowlist` in `authorize()`'s legacy path, so authorization behavior is unchanged until a `Permission` account is supplied. - Exposes the names (`topology-admin` / `resource-admin` / `index-admin`) in the serviceability CLI for `permission set --add` / `--remove`, and adds matching constants to the Go, TypeScript, and Python SDKs. - Documents the flags in `PERMISSION.md` and the serviceability `README.md`. This PR is definition-only; processor enforcement lands in the follow-up 4/4. Stacked on #3206. ## Testing Verification - `authorize()` legacy-mapping unit tests for each new flag (allowed via a foundation member, denied for others). - CLI permission name↔bitmask round-trip tests cover the three new names. Reference: `smartcontract/programs/doublezero-serviceability/PERMISSION.md`
…auth fallback Extract the duplicated trailing-account parsing in topology clear and assign-node-segments into a shared authorize::split_trailing_permission helper, so the variable-length [leading.., payer, system, permission?] layout is parsed in one tested place. Also broaden authorize(): when a payer's auto-injected Permission account exists but does not grant the requested flag, fall back to the legacy allowlist check as long as RequirePermissionAccounts is off. This makes the Some/None branches symmetric until strict mode is enabled, so a foundation (or other legacy-authorized) key is not locked out of an instruction merely because it also holds an unrelated, under-privileged Permission account.
Follow-up: shared trailing-account helper + broadened legacy auth fallbackTwo changes pushed in 1. Extract
|
Summary
foundation_allowlistchecks withauthorize()in topology (create/delete/clear/assign-node-segments) →TOPOLOGY_ADMIN, resource (create/allocate/deallocate/close) →RESOURCE_ADMIN, and index (create/delete) →INDEX_ADMIN.Permissionaccount is read as the optional trailing account; the variable-lengthclearandassign-node-segmentslayouts detect it by PDA match before consuming their link/device lists.Permissionaccount the legacy foundation path still applies, so existing callers (controlplane, current SDK) keep working.execute_authorized_transactionso the payer'sPermissionPDA is appended when it exists onchain.Behavior change: the topology instructions now reject unauthorized callers with
NotAllowed(Custom(8)) instead ofUnauthorized(Custom(22)), sinceauthorize()is the single rejection path; affected tests updated.Depends on the 3/4 PR (stacked).
Testing Verification
test_topology_create_with_permission_account_allowed: a non-foundation key holdingTOPOLOGY_ADMINcreates a topology via itsPermissionaccount — exercises the new authorization path through a real processor.Unauthorized→NotAllowed.Reference:
smartcontract/programs/doublezero-serviceability/PERMISSION.md