Skip to content

feat(frontend/permissions): hide sections + buttons users can't access (closes #365)#534

Closed
cristim wants to merge 7 commits into
feat/multicloud-web-frontendfrom
fix/issue-365-hide-admin-ui
Closed

feat(frontend/permissions): hide sections + buttons users can't access (closes #365)#534
cristim wants to merge 7 commits into
feat/multicloud-web-frontendfrom
fix/issue-365-hide-admin-ui

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 20, 2026

Summary

  • Adds frontend/src/permissions.ts — a hasPermission(verb, resource) helper that consults the /api/auth/session response to derive what the current session can do.
  • Adds frontend/src/permissions.generated.ts — role-default capability sets generated from Go source via cmd/gen-permissions/main.go, so frontend defaults stay in sync with backend RBAC without manual copying.
  • Conditionally hides the sidebar Admin tab, bulk Purchase / Create Plan buttons on Recommendations, plan write actions, Exchange buttons on RI Exchange, and Settings write controls based on session permissions rather than toasting "admin access required" on click.
  • Adds permission-focused Jest test suites for each gated section (plans, recommendations, riexchange, settings) plus a permissions.test.ts unit test for the helper itself.

Test plan

  • cd frontend && npm test — all permission test suites pass
  • Log in as a non-admin user: Admin sidebar tab is hidden, bulk purchase buttons absent, Exchange buttons absent, Settings fields read-only
  • Log in as admin: all controls visible

Closes #365

Summary by CodeRabbit

Release Notes

  • New Features
    • Added role-based permission gating across the application, restricting admin-exclusive features based on user role
    • Settings page now enforces read-only mode for non-admin users
    • Plans, recommendations, and RI Exchange features now respect user permissions for creation, editing, and execution

Review Change Stack

cristim added 7 commits May 14, 2026 03:14
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`.
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/low Minor harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/feat New capability labels May 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This 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.

Changes

Role-based permission gating implementation

Layer / File(s) Summary
Backend-to-frontend permission code generation
cmd/gen-permissions/main.go, frontend/src/permissions.generated.ts, .pre-commit-config.yaml
Go generator converts backend default permission constants into three TypeScript readonly permission sets (ADMIN_PERMS, USER_PERMS, READONLY_PERMS). Pre-commit hook validates generated file stays in sync with backend and generator.
Permission helper module and type contracts
frontend/src/permissions.ts, frontend/src/auth.ts, frontend/src/__tests__/permissions.test.ts
Core canAccess(action, resource) and getRolePermissions(role) helpers with closed-union Action and Resource types. Auth module delegates isAdmin() to the canonical permissions implementation. Comprehensive unit tests cover all roles and permission scenarios.
Shared permission model update
frontend/src/users/userList.ts
Update role-to-permission mapping to use shared getRolePermissions() helper instead of hardcoded role-switch logic, establishing single source of truth.
Plans page permission gating
frontend/src/plans.ts, frontend/src/__tests__/plans-permissions.test.ts, frontend/src/__tests__/plans.test.ts
Gate create/update/delete buttons based on create:plans, update:plans, delete:plans permissions; hide New Plan button and row actions for unauthorized users while keeping History visible. Test mock adds default admin user for consistent test context.
Recommendations page permission gating
frontend/src/recommendations.ts, frontend/src/__tests__/recommendations-permissions.test.ts, frontend/src/__tests__/recommendations.test.ts
Hide Purchase and Create Plan buttons in bottom action box via hidden property based on execute:purchases and create:plans permissions. Test mock adds default admin user and comprehensive role-based button visibility tests.
RI Exchange and Settings permission gating
frontend/src/riexchange.ts, frontend/src/settings.ts, frontend/src/__tests__/riexchange-permissions.test.ts, frontend/src/__tests__/settings-permissions.test.ts
Hide RI Exchange action columns and buttons for non-admin users; gate settings form writes by disabling controls and hiding Save/Reset for non-admin sessions. Comprehensive tests verify visibility gating across all roles.
Navigation and top-level UI gating
frontend/src/index.html, frontend/src/styles/base.css, frontend/src/__tests__/html.test.ts
Add admin-only CSS class to Admin tab button to hide it for non-admin users. Update visibility CSS rule to use display: revert for proper cascade behavior. Add regression test asserting Admin button has correct class.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • LeanerCloud/CUDly#275: Modifies recommendations bottom action box behavior, main PR gates buttons via permission checks while retrieved PR updates them based on purchase target selection.
  • LeanerCloud/CUDly#452: Same permission-gating work across frontend—permissions module, permission helpers, canAccess checks, and admin-only UI class—likely a related or duplicate effort.
  • LeanerCloud/CUDly#299: Adds backend RBAC changes including new approve-own/approve-any permissions that will be reflected in the frontend-generated permissions.generated.ts file.

🐰 Permissions now speak their mind,
Hiding buttons users can't find,
No more toasts of sad "denied"—
Just UI that's clear, focused, and tried! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: implementing permission-based visibility for UI sections and buttons that users cannot access.
Linked Issues check ✅ Passed All acceptance criteria from issue #365 are addressed: canAccess helper created with tests, effectivePermissions refactored, Admin dropdown hidden, Plans/Recommendations/RI Exchange/Settings actions gated, and no permission-denied toasts.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #365 scope: permissions helper, UI gating, and supporting test infrastructure. CSS display fix and pre-commit codegen are necessary supporting changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-365-hide-admin-ui
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/issue-365-hide-admin-ui

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 20, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 20, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

✅ Actions performed

Full review triggered.

@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 20, 2026

Superseded by #452, which merged the identical fix/issue-365-hide-admin-ui payload on 2026-05-14 (commit d448d72). This PR was opened 6 days later still pointing at the pre-squash commits, so its diff against current base is effectively empty (17 of 20 touched files are byte-identical to base; the #365 gating is verified live in base). Closing as a stale duplicate; no work is lost. See #365.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/low Minor harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant