feat(frontend/permissions): hide sections + buttons users can't access (closes #365)#534
feat(frontend/permissions): hide sections + buttons users can't access (closes #365)#534cristim wants to merge 7 commits into
Conversation
Introduce `frontend/src/permissions.ts` with `canAccess(action, resource)` + `getRolePermissions(role)` mirroring the backend's `DefaultAdminPermissions` / `DefaultUserPermissions` / `DefaultReadOnlyPermissions` (`internal/auth/types.go:367-415`). Drive `auth.ts:isAdmin` and `users/userList.ts:effectivePermissions` from the new shared module so the UI badge in the admin Users page and the global UI gates that follow in subsequent commits stay in lockstep. Side-effect: the badge in the Users page now also shows the `cancel-own:purchases` / `retry-own:purchases` / `approve-own:purchases` entries for `user`-role accounts that were previously missing from the local mirror in `effectivePermissions`. The backend already grants these per PR #364; this catches the badge up. Unknown roles previously fell through to `user` defaults in `effectivePermissions`. Now they return the empty permission set, matching the backend's deny-by-default behaviour. No production role falls in this bucket today; the change closes a latent fail-open. Tests enumerate every entry in each role default so a future drift between this mirror and the backend fails CI rather than at runtime.
Mark `#admin-tab-btn` with `admin-only` so the existing `auth.ts:updateUserUI` toggle hides it for non-admin sessions. Before this commit a readonly or user-role login saw the Admin sidebar entry, clicked it, and got a stack of red 403 toasts on every nested API call because the page itself stayed admin-only. Switch `.admin-only.visible` from `display: block` to `display: revert` so each element falls back to its UA / cascade default. Sections still render as block, buttons as inline-block, and the sidebar tab buttons inherit the inline-flex from `.app-sidebar-nav .tab-btn`. With the old `display: block` rule the sidebar Admin button would have flattened the sidebar's flex layout for admins.
…e verb (#365) Gate three classes of buttons on the Plans page by the same permission a click on each one would require, so a non-admin (readonly especially) never sees a button whose only outcome is a 403. * `#new-plan-btn` (top-level "New Plan"): hidden unless `create:plans` (admin + user keep it; readonly loses it). * Plan-card Add Purchases / Edit / toggle-plan: gated by `update:plans` (admin + user keep them; readonly loses all three). * Plan-card Delete: gated by `delete:plans` (admin only). * Plan-card View History stays for every role: read-only inspection. * Planned-purchase row Run / Pause / Resume / Edit: gated by `update:plans`. * Planned-purchase row Disable: gated by `delete:plans`. Permission lookups are cached once per render rather than re-evaluated per card so a 100-plan list doesn't bounce through the helper 600 times. Backend `requirePermission` checks stay untouched. The frontend gates are pure UX defense in depth.
…sessions without the verb (#365) The Opportunities-tab bottom-action box renders two CTAs: `#bulk-purchase-btn` ("Purchase" one-off) and `#create-plan-btn` ("Create Plan"). Both stayed visible for every signed-in session, and a readonly user clicking either got a 403 toast. Gate each CTA by the underlying verb: * Purchase: `execute:purchases` (admin only by default). * Create Plan: `create:plans` (admin + user keep it; readonly loses it). The action box itself stays visible for every role so readonly users still get the selection summary and the capacity input as read-only browsing aids. Only the mutating buttons disappear.
…ns (#365) The convertible-RI table and the reshape-recommendations table both render per-row "Exchange" buttons that hit admin-only backend endpoints. A readonly or user-role session clicking either currently gets a 403 "admin access required" toast. Gate both buttons by `admin:*` (the verb the backend handler requires). For non-admin sessions also drop the Actions column header so the table doesn't render with a dangling empty column.
…ions (#365) The /admin/general and /admin/purchasing sub-tabs are reachable for every signed-in role (only /admin/accounts and /admin/users get the navigation.ts non-admin redirect). Previously a user-role or readonly session could open Settings, edit fields, and only learn the values weren't saved when Save returned 403. Render the form read-only for non-admin sessions: * Disable every input / select / textarea / button under `#global-settings-form`. * Hide `#save-settings-btn` and `#reset-settings-btn` via the HTML hidden attribute. The form stays visible so non-admin sessions can still inspect the configured providers, defaults, and grace windows as a reference. Backend remains authoritative and 403s any attempted write.
The hand-written `frontend/src/permissions.ts` mirrored the backend's DefaultAdminPermissions / DefaultUserPermissions / DefaultReadOnlyPermissions in `internal/auth/types.go`, but `permissions.test.ts` enumerated entries against the TS mirror itself, so a drift in the Go defaults would not be caught by the existing tests. This change splits the data portion (the three default permission sets) into a generated file `permissions.generated.ts` produced by `go run ./cmd/gen-permissions`, which imports `internal/auth` and sorts entries by (action, resource) for a stable diff. The wrapper `permissions.ts` keeps the hand-written closed-union Action / Resource types and the canAccess / isAdmin / getRolePermissions helpers and imports the sets from the generated file. Existing callers in plans.ts, settings.ts, recommendations.ts, riexchange.ts, auth.ts and users/userList.ts plus the permissions.test.ts suite continue to work unchanged. A new `permissions-codegen` pre-commit hook re-runs the generator and `git diff --exit-code` against the committed file so a stale copy fails locally on commit, and CI catches the same drift via the existing pre-commit GitHub Actions workflow that runs `pre-commit run --all-files`.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughThis PR implements comprehensive role-based permission gating across the frontend by adding a backend-to-frontend permission code generator, a TypeScript permission helper module, and permission-aware UI gating throughout plans, recommendations, RI Exchange, settings, and navigation based on user roles and permissions. ChangesRole-based permission gating implementation
Sequence Diagram(s)sequenceDiagram
participant User as Non-Admin User
participant UI as Page UI
participant canAccess as canAccess(action, resource)
participant state as state.getCurrentUser()
participant PermSet as getRolePermissions(role)
User->>UI: Navigate to Plans page
UI->>canAccess: check 'create:plans'
canAccess->>state: read current user role
state-->>canAccess: role='user'
canAccess->>PermSet: lookup 'user' permissions
PermSet-->>canAccess: USER_PERMS (no 'create:plans')
canAccess-->>UI: false
UI->>UI: Hide 'Create Plan' button
UI-->>User: Page renders without create button
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
|
Superseded by #452, which merged the identical |
Summary
frontend/src/permissions.ts— ahasPermission(verb, resource)helper that consults the/api/auth/sessionresponse to derive what the current session can do.frontend/src/permissions.generated.ts— role-default capability sets generated from Go source viacmd/gen-permissions/main.go, so frontend defaults stay in sync with backend RBAC without manual copying.permissions.test.tsunit test for the helper itself.Test plan
cd frontend && npm test— all permission test suites passCloses #365
Summary by CodeRabbit
Release Notes