Catalog: distinguish org-scoped vs scoped permissions for UI gates
- Dominant language
- Go
- Stars
- 55
- Forks
- 16
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 84
Description
## Problem
`useHasPermission(key)` reads from `UserInfo.permissions`, which is a flat union across all scopes (`server/internal/domain/authz/effective.go:188` — `FlatKeys`). UI gates can't tell whether the user has the permission **org-scoped** vs **scoped to a specific resource**.
Most RPCs that the UI calls with empty `ResourceContext` require the **org-scoped** form (e.g. `ListSites`, `ListBuildings`, `ListMinerStateSnapshots`, etc.). So a scoped-only user passes `useHasPermission("...:read")`, the gated component mounts, and the org-scoped RPC returns PermissionDenied — usually surfacing as a "Failed to load X" toast.
This has bitten twice already on the multi-site PR (#368):
- **site:read** — fixed inline by threading `ConnectError` code through `listSites.onError` and using PermissionDenied as an access-blocked signal in `FleetLayout`. Commit `36f8f58b`.
- **miner:read** — flagged by codex on `FleetLayout` for `CompleteSetup`. Currently mitigated by the catalog-level `useHasPermission("miner:read")` gate, but scoped-only `miner:read` users would still get the toast on Fleet tab open.
PR #408 adds a curtailment-specific case: manual curtailment preview/start/stop controls should require org-scoped `curtailment:manage`. A flat `useHasPermission("curtailment:manage")` check may not prove the server will allow the org-scoped public curtailment RPC.
## Proposed fix
Surface org-scoped checks distinctly. Options:
- **A.** Add `useHasOrgPermission(key)` that reads a separate org-only projection from `UserInfo`. Mirror server's `Has(key, emptyResourceContext)` semantics.
- **B.** Extend `useHasPermission` to take a scope: `useHasPermission("miner:read", { scope: "org" })`.
- **C.** Replace the flat union projection entirely with a `{ orgScope: Set, perResource: Map }` shape and require callers to be explicit.
Option B is probably the smallest delta — keep `UserInfo.permissions` flat, add a sibling `UserInfo.orgPermissions` (or similar), and let `useHasPermission(key, { scope: "org" })` consult it.
## Audit
Once the new API exists, sweep all `useHasPermission(...)` call sites against the corresponding RPC's required scope in `server/internal/handlers/middleware/rpc_permissions.go`. Likely candidates beyond site/miner:
- `rack:read` / `rack:manage`
- `pool:read` / `pool:manage`
- `apikey:manage`
- `schedule:read` / `schedule:manage`
- `serverlog:read`
- `activity:read`
- `curtailment:read`
- `curtailment:manage` for manual curtailment preview/start/stop controls after #408
## Acceptance
- New API exists and is documented in `store/hooks/useAuth.ts`.
- All UI gates that map to RPCs requiring org-scoped checks are migrated.
- Curtailment manual preview/start/stop UI checks org-scoped `curtailment:manage`, not only the flat permission union.
- Inline workarounds (the listSites `Code.PermissionDenied` plumbing in `useSites` / `FleetLayout`) can be unwound or simplified.
## References
- PR #368 thread (site:read fix): https://github.com/block/proto-fleet/pull/368#discussion_r3359418149
- PR #368 thread (miner:read raised, deferred to this issue): https://github.com/block/proto-fleet/pull/368#discussion_r3364755424
- Server: `server/internal/domain/authz/effective.go` (FlatKeys + Has)
- Server: `server/internal/handlers/middleware/rpc_permissions.go` (per-RPC permission map)
Contributor guide
Research direction
Start with store/hooks/useAuth.ts and the UserInfo permission projection, then compare server/internal/domain/authz/effective.go with server/internal/handlers/middleware/rpc_permissions.go. Audit the listed useHasPermission call sites and the useSites/FleetLayout workaround. Done means org-scoped UI gates use the new API, including curtailment controls, and the documented acceptance cases are covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, typescript
- Domain
- authorization, backend, frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100