Skip to content

feat(auth): GET /api/auth/me/permissions + wire frontend canAccess - #922

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/me-permissions-endpoint
Jun 3, 2026
Merged

feat(auth): GET /api/auth/me/permissions + wire frontend canAccess#922
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/me-permissions-endpoint

Conversation

@cristim

@cristim cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds GET /api/auth/me/permissions (AuthUser level) that returns the current user's effective permission set derived from the union of their group permissions via the same GetUserPermissions path the backend enforces with
  • Rewrites canAccess(action, resource) in frontend/src/permissions.ts to consult the server-provided set when available; removes the void action; void resource; return isAdmin() stub that was blocking every non-admin user
  • Fetches the permissions on login/bootstrap in app.ts and caches them on the current-user state; best-effort so a failure does not block app loading

Closes #917

Test plan

  • Backend: 6 new handler tests in handler_auth_test.go covering multi-group user gets union, admin gets wildcard, unauthenticated gets 401
  • Frontend: expanded permissions.test.ts with canAccess tests for the effective-permissions path (explicit grants, admin wildcard, multi-group union, empty set) and kept fallback-path tests
  • go test ./internal/... passes (4273 tests)
  • npm test passes (2213 tests, 65 suites)
  • npm run build compiles cleanly
  • npm run lint skipped - pre-existing broken ESLint config (no .eslintrc)

🤖 Generated with claude-flow

Summary by CodeRabbit

  • New Features

    • Added a permissions endpoint and app startup fetch so the UI uses each user’s effective permission set for more accurate access gating; admins are still allowed during initial loading as a fallback.
    • Runtime validation of permissions responses to ensure malformed data is rejected.
  • Tests

    • Expanded test coverage for the permissions endpoint and client-side permission-checking across auth scenarios, wildcards, group unions, empty/absent permissions, and error cases.

closes #917)

Adds a new authenticated endpoint that returns the effective permission
set for the current user (union of all group permissions) so the frontend
can gate UI elements based on real group-derived access instead of the
admin-or-nothing stub that has been blocking every non-admin user since
the group-only authz migration (#912/#914).

Backend:
- Add GET /api/auth/me/permissions (AuthUser level, mirrors /api/auth/me)
- Handler delegates to GetUserPermissionsAPI -> GetUserPermissions (same
  union path the server enforces with), returns {permissions, is_admin}
- Add GetUserPermissionsAPI to AuthServiceInterface + implementation in
  service_api.go via the existing GetUserPermissions union logic
- Register route in router.go; add adapter in server/app.go; add mock
  method + 6 handler tests covering multi-group user, admin wildcard,
  and 401 paths

Frontend:
- Add getUserPermissions() in api/auth.ts and re-export via api/index.ts
- Extend User type with optional effectivePermissions field
- Rewrite canAccess() to consult effectivePermissions when present;
  falls back to admin group-membership check while loading (fails closed)
- Fetch permissions in app.ts bootstrap after getCurrentUser(), merge
  onto current-user state; best-effort (errors do not block app loading)
- Update permissions.test.ts: add canAccess tests for the effective-
  permissions path (multi-group union, admin wildcard, empty set);
  keep fallback-path tests with updated comments
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/high Significant harm urgency/this-sprint Within the current sprint impact/all-users Affects every user effort/m Days type/bug Defect labels Jun 2, 2026
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5bc45f3-2b94-4a97-b6c6-136c4bb31b3b

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee652e and 90fb7f6.

📒 Files selected for processing (4)
  • frontend/src/__tests__/api-permissions.test.ts
  • frontend/src/api/auth.ts
  • internal/api/handler_auth.go
  • internal/api/handler_auth_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/api/auth.ts

📝 Walkthrough

Walkthrough

Adds GET /api/auth/me/permissions; backend returns the authenticated user’s effective permissions. Frontend fetches and caches that set at bootstrap and updates canAccess() to consult effectivePermissions (admin:* still grants all; fallback to isAdmin() while loading).

Changes

Permissions Endpoint and Frontend Gating

Layer / File(s) Summary
Backend API contract
internal/api/types.go
AuthServiceInterface gains GetUserPermissionsAPI(ctx, userID); new PermissionEntry and UserPermissionsResponse DTOs model the endpoint response.
Backend service adapter
internal/auth/service_api.go, internal/server/app.go
Auth service adapter implements GetUserPermissionsAPI by converting internal permissions to wire format; server app adapter exposes the call.
Handler and auth resolution
internal/api/handler_auth.go
Handler authenticates caller (session or API key), resolves user ID, calls GetUserPermissionsAPI, validates and serializes the payload, computes IsAdmin, and returns UserPermissionsResponse.
Router wiring
internal/api/router.go
Register authenticated GET /api/auth/me/permissions route and add wrapper handler forwarding to the implementation.
Backend tests and mocks
internal/api/handler_auth_test.go, internal/api/mocks_test.go, internal/api/handler_ri_exchange_test.go
Add handler tests covering missing/invalid auth, regular/admin success cases, unexpected payload regression, API-key auth; mocks implement GetUserPermissionsAPI.
Frontend API types and client function
frontend/src/api/types.ts, frontend/src/api/auth.ts, frontend/src/api/index.ts
Add PermissionEntry and UserPermissionsResponse types; extend User with optional effectivePermissions; implement getUserPermissions() with runtime shape validation and export via barrel.
Frontend permission fetch on bootstrap
frontend/src/app.ts
After loading current user, call getUserPermissions() and merge effectivePermissions into user state; log and ignore failures.
Frontend permission checking logic
frontend/src/permissions.ts
canAccess() consults user.effectivePermissions for admin:* or action/resource matches (resource may be *); while undefined, it falls back to isAdmin().
Frontend permission tests
frontend/src/__tests__/permissions.test.ts
Restructured tests cover loading-race fallback, effective-permissions grants/denials, wildcard admin behavior, empty-array denial, multi-group union behavior, and null-user denial; mockUserWithGroups extended.
Frontend getUserPermissions() tests
frontend/src/__tests__/api-permissions.test.ts
Jest tests validate success parsing and detailed rejection messages for malformed API responses.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Authenticated Client
  participant Handler as Handler
  participant Service as Auth Service

  Client->>Handler: GET /api/auth/me/permissions
  Handler->>Handler: Extract credentials (bearer/X-API-Key)
  Handler->>Handler: Validate session or validate API key
  Handler->>Service: GetUserPermissionsAPI(userID)
  Service-->>Handler: []APIPermission
  Handler->>Handler: Convert to PermissionEntry / Compute IsAdmin
  Handler-->>Client: UserPermissionsResponse{permissions, is_admin}
Loading
sequenceDiagram
  participant App as Frontend App
  participant API as API Client
  participant Backend as Backend
  participant State as User State

  App->>API: getUserPermissions()
  API->>Backend: GET /api/auth/me/permissions
  Backend-->>API: UserPermissionsResponse
  API-->>App: {permissions, is_admin}
  App->>State: store effectivePermissions
  Note over State: Later...
  rect rgba(100, 150, 200, 0.5)
  App->>App: canAccess(action, resource)
  App->>State: check effectivePermissions
  State-->>App: PermissionEntry[] or undefined
  App->>App: match admin:* or action/resource (resource may be '*') or fallback isAdmin()
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • LeanerCloud/CUDly#452: Original permissions test coverage for canAccess() and isAdmin() behavior; this PR restructures and extends that test suite to validate permission-based gating instead of stub admin-only checks.
  • LeanerCloud/CUDly#726: Changes related to frontend permission expectations and router/handler-level gating that overlap with permission checks and tests.

Suggested labels

type/feat

Poem

🐰 Hoppity hooray — permissions now flow,
The frontend fetches what backends bestow.
Admins keep their star; groups grant the rest,
Buttons reappear for each rightful guest.
A rabbit cheers softly — the UI's refreshed, at best! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% 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 PR title 'feat(auth): GET /api/auth/me/permissions + wire frontend canAccess' clearly and concisely summarizes the main changes: adding a new auth endpoint and wiring frontend permission logic.
Linked Issues check ✅ Passed The PR fully implements all core requirements from #917: adds GET /api/auth/me/permissions endpoint returning effective permissions, fetches/caches permissions on frontend login, updates canAccess() to consult server permissions with fallback, includes comprehensive tests for union behavior and validation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #917 requirements: backend permission endpoint, frontend permission fetching, canAccess() logic, type definitions, route registration, and related tests—no unrelated refactoring or scope creep detected.

✏️ 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 feat/me-permissions-endpoint

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

@cristim

cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ 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

cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/api/auth.ts`:
- Around line 114-124: getUserPermissions currently returns
apiRequest<UserPermissionsResponse> without runtime validation which can let
malformed responses (non-array permissions, nulls, or wrong types) crash
canAccess; update getUserPermissions to perform shape checks following the
existing pattern used by getResetTokenStatus/setupMFA/etc.: verify the response
is an object, that response.permissions is an array, every entry has string
action and resource, and response.is_admin is a boolean, and throw a descriptive
error if any check fails so the caller (app.ts) falls back to the safe gating
logic.

In `@internal/api/handler_auth.go`:
- Around line 115-123: The handler currently forces a bearer session by calling
extractBearerToken and h.auth.ValidateSession even for requests already
authenticated via the router's AuthUser mode; change the logic so that if
extractBearerToken(req) returns empty you resolve the authenticated user from
the request/auth context (instead of returning a 401) and only call
h.auth.ValidateSession(ctx, token) when a bearer token is present; update the
code paths that relied on the returned session to accept the resolved
user/session object from the request context so AuthUser and API-key callers are
honored.
- Around line 130-146: The unchecked type assertion of raw to
[]auth.APIPermission should fail loudly instead of silently returning empty
permissions; update the code around the GetUserPermissionsAPI handling to
perform a checked assertion (apiPerms, ok := raw.([]auth.APIPermission)) and if
ok is false return an error (wrapped with context) rather than continuing, so
callers of UserPermissionsResponse can surface the server-side type mismatch;
keep building PermissionEntry and IsAdmin only after a successful assertion and
reference PermissionEntry, UserPermissionsResponse, and auth.APIPermission in
your fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dce2af26-d554-4f76-b8fd-176b01849f58

📥 Commits

Reviewing files that changed from the base of the PR and between 1dbeef7 and 4ee652e.

📒 Files selected for processing (14)
  • frontend/src/__tests__/permissions.test.ts
  • frontend/src/api/auth.ts
  • frontend/src/api/index.ts
  • frontend/src/api/types.ts
  • frontend/src/app.ts
  • frontend/src/permissions.ts
  • internal/api/handler_auth.go
  • internal/api/handler_auth_test.go
  • internal/api/handler_ri_exchange_test.go
  • internal/api/mocks_test.go
  • internal/api/router.go
  • internal/api/types.go
  • internal/auth/service_api.go
  • internal/server/app.go

Comment thread frontend/src/api/auth.ts
Comment thread internal/api/handler_auth.go Outdated
Comment thread internal/api/handler_auth.go
cristim added 2 commits June 2, 2026 20:52
…ayload

CR #922 findings F1 + F2 on GET /api/auth/me/permissions, addressed
together because they share the handler body:

F1 — Unchecked type assertion on GetUserPermissionsAPI's `any` return
swallowed adapter/type mismatches and degraded to an empty
PermissionEntry slice. The frontend then renders the caller as having
lost all access, hiding the real server bug. Switch to a checked
assertion + 500 with a logged "GetUserPermissionsAPI returned %T,
want []auth.APIPermission" line.

F2 — The route is registered with Auth: AuthUser, which admits the
stateless admin API key, any user API key, OR a bearer-token session
(see router.go AuthLevel doc). The previous body re-required a bearer
session after the router had already admitted the caller, so a valid
admin / user API-key request would still 401 on this endpoint. Resolve
the user ID from any of the three credentials via a new
resolveAuthenticatedUserID helper, then look up permissions. The
admin API key has no backing user row, so it short-circuits to
{admin, *} + is_admin=true (matches the requireAdmin /
requirePermission convention).

Regression tests:
- TestHandler_getCurrentUserPermissions_UnexpectedPayload (F1)
- TestHandler_getCurrentUserPermissions_AdminAPIKey (F2)
- TestHandler_getCurrentUserPermissions_UserAPIKey (F2)
CR #922 finding F3. getUserPermissions() previously cast the response
directly to UserPermissionsResponse with no shape check. canAccess()
later iterates user.effectivePermissions with `for (const p of ...)`
and reads p.action / p.resource — a non-array `permissions`, a null
entry, or a non-string action/resource throws synchronously outside
the app.ts fetch/merge try/catch and crashes the bootstrap path.

Align with the existing pattern in this file (getResetTokenStatus,
setupMFA, enableMFA, regenerateMFARecoveryCodes): validate the
response is an object, permissions is an array, every entry is an
object with string action and string resource, and is_admin is a
boolean. Throw a descriptive error on any mismatch so the caller in
app.ts falls back to the safe group-membership gating.

Adds frontend/src/__tests__/api-permissions.test.ts covering the
happy path plus each rejection branch (non-object response, null
response, non-array permissions, null entry, missing action,
non-string resource, missing is_admin, non-boolean is_admin).
@cristim

cristim commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

Addressed all 3 CR findings:

  • F1 (handler_auth.go:146): checked type-assert on GetUserPermissionsAPI's any payload; unexpected shape now returns 500 with a logged returned %T, want []auth.APIPermission line instead of silently degrading to empty permissions.
  • F2 (handler_auth.go:123): handler now honors AuthUser auth (admin API key, user API key, or bearer session) via a new resolveAuthenticatedUserID helper; admin-API-key short-circuits to {admin, *}+is_admin=true.
  • F3 (auth.ts:124): getUserPermissions() now runtime-validates the response shape (response is object, permissions is array of {action:string, resource:string}, is_admin is boolean) mirroring setupMFA/getResetTokenStatus, so canAccess() can't crash on a malformed payload.

Regression tests added for each (3 Go tests in handler_auth_test.go, 10 TS tests in new api-permissions.test.ts).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough summary! All three fixes look well-handled:

  • F1 — Failing loudly with a 500 + log on unexpected type is much safer than silently returning empty permissions.
  • F2 — Centralizing auth resolution in resolveAuthenticatedUserID and short-circuiting admin API keys to {admin,*} is the right approach.
  • F3 — Runtime shape validation mirrors the existing patterns in the codebase and prevents canAccess() from blowing up on a malformed payload.

Triggering a fresh review now to cover the new changes.

(ノ◕ヮ◕)ノ*:・゚✧

✅ 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
cristim merged commit 6943207 into feat/multicloud-web-frontend Jun 3, 2026
6 checks passed
@cristim
cristim deleted the feat/me-permissions-endpoint branch June 3, 2026 10:28
cristim added a commit that referenced this pull request Jun 3, 2026
 #660)

RI exchanges are financially irreversible once submitted to AWS. Keeping
execute:ri-exchange disjoint from execute:purchases ensures a user who
can initiate regular purchases cannot inadvertently (or intentionally)
trigger an RI exchange without an explicit additional grant.

Changes:
- Add ResourceRIExchange = "ri-exchange" constant to internal/auth/types.go
- Update executeExchange handler to require execute:ri-exchange (was
  execute:purchases)
- Update TestExecuteExchange_PermissionGate to assert the new permission
  verb; add isolation sub-test proving execute:purchases does not grant
  ri-exchange access
- Add /api/ri-exchange/* routes to openapi.yaml with 403 responses and
  permission documentation on the execute endpoint
- Add 'ri-exchange' to the frontend Resource closed-union type and
  extend canAccess tests: admin true, user false (no default grant);
  add isolation sub-tests proving execute:purchases does not imply
  execute:ri-exchange (and vice versa) on the frontend gating mirror

Rebased onto feat/multicloud-web-frontend post #922 (effectivePermissions
migration). The conflict in frontend/src/__tests__/permissions.test.ts
was resolved by re-expressing the user-default-deny invariant in the new
effective-permissions model: a non-admin holding execute:purchases is
still denied execute:ri-exchange. Em-dashes scrubbed from added prose.

No DB migration needed: role defaults are computed at runtime from
Go constants; no DB table stores the permission set.

No conflict with PR #884 (fix/476): that PR adds 403 to plans/purchases
routes; this PR adds a new ri-exchange section that #884 does not touch.
cristim added a commit that referenced this pull request Jun 3, 2026
 #660) (#891)

RI exchanges are financially irreversible once submitted to AWS. Keeping
execute:ri-exchange disjoint from execute:purchases ensures a user who
can initiate regular purchases cannot inadvertently (or intentionally)
trigger an RI exchange without an explicit additional grant.

Changes:
- Add ResourceRIExchange = "ri-exchange" constant to internal/auth/types.go
- Update executeExchange handler to require execute:ri-exchange (was
  execute:purchases)
- Update TestExecuteExchange_PermissionGate to assert the new permission
  verb; add isolation sub-test proving execute:purchases does not grant
  ri-exchange access
- Add /api/ri-exchange/* routes to openapi.yaml with 403 responses and
  permission documentation on the execute endpoint
- Add 'ri-exchange' to the frontend Resource closed-union type and
  extend canAccess tests: admin true, user false (no default grant);
  add isolation sub-tests proving execute:purchases does not imply
  execute:ri-exchange (and vice versa) on the frontend gating mirror

Rebased onto feat/multicloud-web-frontend post #922 (effectivePermissions
migration). The conflict in frontend/src/__tests__/permissions.test.ts
was resolved by re-expressing the user-default-deny invariant in the new
effective-permissions model: a non-admin holding execute:purchases is
still denied execute:ri-exchange. Em-dashes scrubbed from added prose.

No DB migration needed: role defaults are computed at runtime from
Go constants; no DB table stores the permission set.

No conflict with PR #884 (fix/476): that PR adds 403 to plans/purchases
routes; this PR adds a new ri-exchange section that #884 does not touch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/all-users Affects every user priority/p2 Backlog-worthy severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant