ADORSYS-GIS / ADORSYS-GIS/lightbridge-authz
[Ticket]: Budget balance/refill ladder have no read path for a non-home owned/member account
- Langage dominant
- Rust
- Étoiles
- 0
- Forks
- 1
- Merge moyen
- 6 h 42 min
- PR mergées (30 j)
- 246
Description
## Type
Bug
## Summary
We need to add an `accountId`-scoped, member-callable way to read a second (non-home) account's budget balance and refill ladder because the current self-service procedures structurally cannot answer for anything but the caller's home account.
Expected result:
> A member of a non-home account they own or belong to (`accountId != auth().id`) can read that account's `BudgetBalance` and refill ladder — and, ideally, submit a self-service refill for it — through a `budget:read-own`/`budget:self-refill`-gated procedure, the same permission tier `getMyBudgetBalance`/`getMyBudgetRefillLadder` already use for the home account today.
## Intent
ADR-0026 (lightbridge-authz#564) lets one identity own many accounts — a home account (`id == subject == userId`) plus any number of minted-id second accounts. The console's account-scoping audit (converse-frontends#392, off the epic converse-frontends#368) found that the budget domain never got the equivalent update: `getMyBudgetBalance`/`getMyBudgetRefillLadder`/`requestBudgetRefill`'s companion read (`getMyBudgetRefillLadder`) take **no target account argument at all** — they always resolve to `auth().id`, i.e. the JWT subject, which is only ever the caller's home account.
The only account-targeted alternative, `getBudgetBalance(budgetAccountId, period)`, is gated at the operator-only `budget:read` permission (`Permission::BudgetRead`), not `budget:read-own` — a normal (non-admin) owner or member of their own second account cannot use it. Concretely: today there is no way for an ordinary user to see their second account's budget balance, or its refill ladder, without being a platform operator.
If this is intentional (e.g. multi-account budgeting is deliberately out of scope for now), the frontend's plan is to keep showing an honest "unavailable" state (already shipped in converse-frontends#392) rather than silently displaying the home account's numbers under the second account's label — so this ticket is either "add the read path" or "confirm this is permanently out of scope" so the frontend gap note can be updated accordingly.
## Source of truth
- converse-frontends#368 (epic: Console UI/UX revamp, phase 8 "backend gap issues")
- converse-frontends#392 (IA v3 phase 2d: the account-scoping audit that found this)
- ADR-0026, lightbridge-authz#564 ("one identity may own many accounts")
- `crates/lightbridge-authz-api/schema/authz.cstack:1261-1292,1447-1482` (`getMyBudgetRefillLadder`, `getMyBudgetBalance`, `getBudgetBalance`)
## Current Behavior
- `getMyBudgetBalance(period)` and `getMyBudgetRefillLadder(period)` take no account argument; both resolve to `auth().id` (the caller's JWT subject == home account id) unconditionally, per their own schema doc comments in `authz.cstack`.
- `getBudgetBalance(budgetAccountId, period)` exists and does take a target account, but is gated at `budget:read` — held only by an operator role (`lightbridge-admin` in the default role mapping), not by a plain account owner/member.
- There is no `getBudgetRefillLadder(budgetAccountId, period)` equivalent at all, admin or otherwise.
- Net effect: a caller who owns (or is a project member of) a second, non-home account has no procedure, at any permission tier they can hold, that reads that account's budget balance or refill ladder.
## Expected Behavior
- A `budget:read-own`-gated procedure (or an `accountId`-aware variant of the existing one) that returns `BudgetBalance` for a caller-supplied `accountId`, restricted server-side to accounts the caller actually owns or is a member of (mirroring the ownership/membership check `ApiKey`/`Project` already carry via `@@allow`).
- The equivalent for the refill ladder (`MyBudgetRefillLadder`), and ideally `requestBudgetRefill` validated against that same per-account ladder rather than implicitly assuming the home account's.
## Acceptance Criteria
- [ ] Given a caller who owns a second, non-home account, when they call the new/updated procedure with that account's id, then they receive its real `BudgetBalance` (not a 403, not the home account's numbers).
- [ ] Given a caller who is a project MEMBER (not owner) of a second account, when they call the same procedure, then the read succeeds if `budget:read-own`-equivalent access is intended to cover members too (confirm the intended boundary — owner-only vs owner+member — as part of this ticket, matching whatever `ApiKey`'s own `@@allow` uses as precedent).
- [ ] Given a caller with no relationship to the target account, when they call the procedure, then it is refused (403/unauthorized), never silently returning another account's data.
- [ ] Existing `getMyBudgetBalance`/`getMyBudgetRefillLadder`/`getBudgetBalance` behavior is unchanged (this is additive, not a breaking change to the home-account or admin paths).
- [ ] Relevant tests are added or updated (schema `@@allow` coverage + service-layer test for the new ownership/membership check).
- [ ] Verification evidence is provided.
## Out of Scope
- Any change to admin (`budget:read`) behavior.
- UI work — the console side already ships an honest "unavailable for this account" fallback (converse-frontends#392); wiring it to a real read is a follow-up frontend ticket once this lands.
## Technical Context
- `crates/lightbridge-authz-api/schema/authz.cstack`: `GetMyBudgetBalanceInput`/`getMyBudgetBalance` (~line 1462-1473), `GetMyBudgetRefillLadderInput`/`getMyBudgetRefillLadder` (~line 1281-1292), `GetBudgetBalanceInput`/`getBudgetBalance` (~line 1474-1483), `RequestBudgetRefillInput`/`requestBudgetRefill` (~line 1249-1259).
- `ApiKey`'s own `@@allow("read", (project.account.userId == auth().id || project.members.some.accountId == auth().id) && ...)` (authz.cstack:428) is the closest existing precedent for an ownership-OR-membership schema predicate, if that is the intended boundary here.
- Frontend context: converse-frontends#392 (`apps/console/src/containers/account-ownership.ts`'s new `isHomeAccount`, `apps/console/src/containers/use-budget-refill.ts`, `apps/console/src/containers/use-overview-screen.ts`).
## Risks
- Scope-creep risk: this ticket is intentionally narrow (read path only). Extending `requestBudgetRefill`'s own validation to a non-home account's ladder is a real but separate follow-up once the read exists.
- If "second-account budgeting" turns out to be deliberately out of scope for the product, this ticket should be closed as won't-fix with that decision recorded here, rather than left open indefinitely — the frontend already degrades honestly either way.
## Test Plan
- Schema-level `@@allow` test(s) for the new/updated procedure.
- Service-layer test: owner of a second account can read its balance/ladder; a stranger cannot; the home-account path is unaffected.
- `cargo test` in the affected crate(s).
## Verification evidence
Filed from a live account-scoping audit (converse-frontends#392) that traced the exact gap through the schema (`authz.cstack`) and confirmed, by reading every budget procedure's own `@allow` clause and doc comments, that no `budget:read-own`-reachable path exists for a non-home account today. No code change proposed in this ticket — it documents the gap for `lightbridge-authz` to evaluate and prioritize.
## Human accountable owner
@stephane-segning
## AI Usage Declaration
- Understanding code
- Drafting the ticket
## Human verification completed
- [x] I understood the intent
- [x] I checked the source of truth
- [x] I reviewed all AI-generated text/code
- [ ] I am the accountable owner and accept responsibility for this ticket. (pending @stephane-segning's own sign-off — filed by an AI agent per converse-frontends#392's audit, not yet human-reviewed)
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.