feat(auth): GET /api/auth/me/permissions + wire frontend canAccess - #922
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds 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). ChangesPermissions Endpoint and Frontend Gating
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}
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 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)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
frontend/src/__tests__/permissions.test.tsfrontend/src/api/auth.tsfrontend/src/api/index.tsfrontend/src/api/types.tsfrontend/src/app.tsfrontend/src/permissions.tsinternal/api/handler_auth.gointernal/api/handler_auth_test.gointernal/api/handler_ri_exchange_test.gointernal/api/mocks_test.gointernal/api/router.gointernal/api/types.gointernal/auth/service_api.gointernal/server/app.go
…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).
|
Addressed all 3 CR findings:
Regression tests added for each (3 Go tests in handler_auth_test.go, 10 TS tests in new api-permissions.test.ts). @coderabbitai review |
|
Triggering a fresh review now to cover the new changes. (ノ◕ヮ◕)ノ*:・゚✧ ✅ Actions performedReview triggered.
|
#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.
#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.
Summary
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 sameGetUserPermissionspath the backend enforces withcanAccess(action, resource)infrontend/src/permissions.tsto consult the server-provided set when available; removes thevoid action; void resource; return isAdmin()stub that was blocking every non-admin userapp.tsand caches them on the current-user state; best-effort so a failure does not block app loadingCloses #917
Test plan
handler_auth_test.gocovering multi-group user gets union, admin gets wildcard, unauthenticated gets 401permissions.test.tswith canAccess tests for the effective-permissions path (explicit grants, admin wildcard, multi-group union, empty set) and kept fallback-path testsgo test ./internal/...passes (4273 tests)npm testpasses (2213 tests, 65 suites)npm run buildcompiles cleanlynpm run lintskipped - pre-existing broken ESLint config (no.eslintrc)🤖 Generated with claude-flow
Summary by CodeRabbit
New Features
Tests