ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz
[Ticket]: Operator-privileged all-accounts enumeration for the admin estate
- 主要言語
- Rust
- スター
- 0
- フォーク
- 1
- 平均マージ
- 6時間 42分
- マージ済み PR(30日)
- 246
説明
### Type
Feature
### Summary
We need to add an operator-privileged "list every account in the system" enumeration procedure (paginated) because the current `Account` model's generic list verb (`model.Account.list`, `authz.cstack:244`) is scoped unconditionally by `@@allow("read", (userId == auth().id) && auth().rpcScope == "crud" && auth().permAccountRead == true)` — self-ownership only, with **no operator/admin bypass anywhere in the schema**. The Lightbridge console's new `/admin/overview` operator dashboard (converse-frontends#368) is supposed to give an operator a true estate-wide view ("overview for ALL account, not just the one the user is bound to. ALL of them." — verbatim owner finding), but today it can only discover the accounts in the operator's own family, because that is the only account enumeration the console has any RPC path to.
Expected result: a new procedure (or a generic-verb policy widening, whichever fits cratestack's model better) that lets a caller holding an operator-level account-read permission list every account in the system, paginated, the same shape `listPendingAugmentationRequests`'s `budgetAccountId: null` case already uses for "the whole cross-account queue, not just mine" (`authz.cstack:1294-1299`).
### Intent
The console's admin estate boards (spend, budget pressure, top spenders, adoption, latency) are currently built by fanning out per-account usage/budget queries over whatever account ids the console can discover. Without a true admin enumeration endpoint, that fan-out is silently capped to the operator's own account family (up to ~100 accounts via `model.Account.list`) plus, since the converse-frontends PR accompanying this ticket (branch `claude/admin-all-accounts`), whatever account ids happen to show up in the global pending refill queue (`listPendingAugmentationRequests({budgetAccountId: null})`) — a real but incidental and partial signal, not a real enumeration. Every account that has never filed a refill request and does not belong to the logged-in operator's own family is invisible to this dashboard, contradicting the dashboard's own stated purpose. This ticket exists so the frontend can stop working around a genuine backend gap and start rendering an honestly complete "ALL accounts" estate view.
### Source of truth (links)
https://github.com/ADORSYS-GIS/converse-frontends/issues/368 (the admin-area request: "Since I'm an admin, I should also have a block /admin for admin stuffs...", later sharpened by the owner's verbatim review finding quoted in the Intent above)
### Current Behavior
`model.Account` (`authz.cstack:132-245`) carries exactly one `@@allow` clause:
```
@@allow("read", (userId == auth().id) && auth().rpcScope == "crud" && auth().permAccountRead == true)
```
There is no second clause, no operator/admin permission flag, and no dedicated `procedure listAllAccounts(...)` anywhere in `schema/authz.cstack`. Confirmed by exhaustive grep across every `@@allow`/`procedure` declaration in the file — the only other cross-tenant admin reads in the whole schema are `getBudgetBalance`/`listBudgetGrants` (budget domain, `authz.cstack:1479-1482` and `1541-1544`), and BOTH require the caller to already know the target `budgetAccountId` — neither one enumerates accounts either. `listPendingAugmentationRequests` (`authz.cstack:1294-1320`) is the only procedure in the schema that reveals account ids beyond the caller's own family, and only for accounts with at least one PENDING refill request — not a general enumeration.
### Expected Behavior
An operator holding the appropriate permission (a new `account:read-all`/`account:list-all` style flag, or reuse of the existing admin `*` role mapping — implementer's call) can call a procedure that pages through every account in the system, the same `Page` (`items`, `totalCount`, `pageInfo`) shape every other `@@paged` list already returns.
### Acceptance Criteria
- [ ] Given a caller with the new operator permission, when they call the new procedure with no filter, then they receive a paginated page of every account in the system, not just their own family.
- [ ] Given a caller WITHOUT the new permission, when they call the same procedure, then they are refused (same `@allow`-clause fail-closed pattern every other admin procedure in this schema already follows).
- [ ] The existing self-scoped `model.Account.list` behavior (and its `@@allow` clause) is completely unchanged — this is an ADDITIVE admin-only surface, not a widening of the self-service one.
- [ ] Pagination is real (`totalCount`/`pageInfo`, or the cursor shape `listPendingAugmentationRequests` already uses) — no unbounded single response.
- [ ] Relevant tests are added or updated (schema policy tests + the Rust-side RBAC test suite, mirroring `budget_gated_op_ids_are_unreachable_on_authz_api_even_for_an_admin`-style coverage already in this repo).
- [ ] Verification evidence is provided.
### Out of Scope
- Widening `model.Account`'s own generic `@@allow("read", ...)` clause in place (this should be a distinct, additive admin surface — same reasoning `listMyBudgetGrants`/`listBudgetGrants` and `getMyBudgetBalance`/`getBudgetBalance` already established: self and admin stay separate procedures/verbs, never one verb with an implicit privilege escalation).
- Any usage-backend/usage-api change — this ticket is authz-api/account-enumeration only. The console's usage-query path is a separate, already-fixed gap (converse-frontends' own `usage-scope-guard.ts` now grants operators a role-gated bypass for `scope: 'account'` usage queries, but that bypass only has real accounts to query once this ticket ships a real enumeration).
- Any frontend work — the console-side workaround (family + refill-queue-derived account ids, capped and captioned) is already shipped in converse-frontends and will be swapped for a real fan-out once this lands.
### Technical Context
- `schema/authz.cstack` — `model Account` (`~line 132`, `@@allow` at `~line 244`), the admin budget-read pair for the pagination/permission shape to mirror (`~line 1479` `getBudgetBalance`, `~line 1541` `listBudgetGrants`), and `listPendingAugmentationRequests`'s `budgetAccountId: String?` optional-target convention (`~line 1294`) for "omitted means everyone" precedent.
- `crates/lightbridge-authz-rest/src/rpc_authorize.rs` — where `model.Account.create`/`model.Account.update` are noted as denied unconditionally at the RBAC layer; the new procedure's coarse permission gate belongs in the same layer/pattern.
- `docs/rbac.md` — permission catalogue; a new `account:*` permission needs registering there and in the frontend's client-side mirror (`packages/hooks/src/rbac.ts` in converse-frontends, "UI convenience, not a security boundary" per its own doc comment).
### Risks
- An unbounded or unfiltered admin account listing is a real information-disclosure surface if the permission gate is misconfigured — follow the exact same fail-closed `@@allow`/RBAC pattern already proven out for the admin budget-read pair, not a new one.
- If cratestack's schema language cannot express "list all, permission-gated, paginated" as a generic verb widening (same class of limitation the budget domain's own doc comments describe for per-tenant predicates), this likely needs a hand-written procedure with hand-written SQL, mirroring `createAccount`/`deleteAccountPermanently`.
### Test Plan
Schema-policy tests plus the Rust-side authorization test suite: a caller WITH the new permission gets every account; a caller WITHOUT it gets denied; the existing self-scoped `model.Account.list` path is unaffected (regression test).
### Verification evidence
Filed from converse-frontends via `gh issue create` against ADORSYS-GIS/lightbridge-authz, as a dev ticket referencing this repo's own investigation (converse-frontends' `claude/admin-all-accounts` branch: full-schema grep confirming no admin account enumeration exists, cited by line number above). No code in THIS repo (lightbridge-authz) has been changed by that investigation — this ticket is the record of the gap, not a fix.
### Human accountable owner
@stephane-segning
### AI Usage Declaration
- Drafting the ticket
- Understanding code
- Reviewing the diff
### Human verification completed
- [x] I understood the intent
- [x] I checked the source of truth
- [x] I reviewed all AI-generated text/code
- [x] I checked for hallucinated assumptions
- [x] I documented remaining risks
- [x] I am the accountable owner and accept responsibility for this ticket.
コントリビューションガイド
評価
この issue はまだ評価されていません。