Repository navigation
fix(api/auth): /me/permissions returns owner's full perms instead of API key's effective scope #62
Description
Summary
GET /api/auth/me/permissions (the getCurrentUserPermissions handler) returns the owning user's full permission set when authenticated via a scoped user API key, instead of the key's effective (intersected) permissions. With PR LeanerCloud/cloud-commitments-cli#1226 now making scoped keys usable on the request path, this endpoint becomes a UI-misleading information leak: a view:recommendations-only key gets told it holds admin:* and is_admin: true from the owner.
Repro
- User U is an Administrators-group member (holds
admin:*). - U creates an API key K scoped to
view:recommendationsonly. - K calls
GET /api/auth/me/permissionswithx-api-key: <K>.
Today's behavior (post-LeanerCloud/cloud-commitments-cli#1226):
- The handler calls
resolveAuthenticatedUserIDwhich returns U's userID. - It then calls
GetUserPermissionsAPI(ctx, U.UserID)returning U's group-derived permissions — the full admin set. - Response:
{ permissions: [{admin, *}], is_admin: true }.
K's actual reachable surface is view:recommendations. The endpoint says otherwise.
Why it matters
- Frontend gating:
is_admin: true+admin:*is exactly what the SPA reads to decide whether to render the Admin tab, surface user-management actions, expose the rekey UI, etc. A key-holder is shown a UI they cannot operate (every admin action then 403s atrequirePermission— which is the correct defense in depth — but the UI lied about the surface). - Trust calibration: a CLI / external integration that consumes this endpoint to decide which calls to attempt will fan out to admin endpoints and absorb a wave of 403s.
- Defense-in-depth posture: the rest of PR fix(api): enforce user API key scoped permissions in requirePermission cloud-commitments-cli#1226 carefully intersects key scope with owner perms on the request path. This single endpoint quietly returns the un-intersected set, defeating the abstraction at the most visible advertisement point.
Note: this is not an authorization bypass — requirePermission correctly blocks admin actions for scoped keys (verified by TestRequirePermission_UserAPIKey / regression LeanerCloud/cloud-commitments-cli#1142: scoped key is denied an out-of-scope permission with 403). The issue is information-shape, not access control.
Proposed fix
When resolveAuthenticatedUserID resolves via a user API key, the handler should return the key's effective permissions (intersection), not the user's full set.
Sketch:
- Add
auth.Service.GetAPIKeyEffectivePermissionsAPI(ctx, apiKey) ([]Permission, error)that wrapsValidateUserAPIKey+ComputeEffectivePermissions. - In
getCurrentUserPermissions, branch on the credential type: if anx-api-keymatches a user key, return its effective permissions; otherwise current bearer-session path. is_adminshould be derived from the effective set, not the user row.
Acceptance
- Scoped key
K->/me/permissionsreturns the SAME permission set the request-path enforcement would honor forK(intersection of key scope and owner group perms). is_admin: trueonly when the effective set actually contains{admin, *}.- Admin API key path unchanged (still returns
{admin, *} + is_admin: true). - Bearer-session path unchanged.
- New test:
TestGetCurrentUserPermissions_UserAPIKey_ReturnsEffectiveScope.
Surfaced by
Adversarial review of PR LeanerCloud/cloud-commitments-cli#1226 (issue LeanerCloud/cloud-commitments-cli#1142).
Findings from the 2026-09-02 codebase audit
Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.
A02-024 (low)
The sibling endpoint has the opposite problem. getCurrentUser (internal/api/handler_auth.go:85-88) calls requireSessionPrincipal, which returns 401 "session authentication required" whenever principal.Session is nil (middleware.go:447-451), so a client authenticating with a user API key cannot read its own id or email at all. getCurrentUserPermissions at :114-121 resolves the same principal through resolveAuthenticatedUserID, and its comment at :107-112 names the API-key case explicitly. Whatever you settle on for the effective-scope semantics here, /api/auth/me should use the same resolver and special-case apiKeyAdminUserID the way the permissions handler does. Finding A02-024.
Activity
cristim commentedon Jul 28, 2026
Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have been fixed or moved.Re-derived independently during the 2026-07-28 full review of
internal/apiand confirmed still live onbe11bdcb5(that is, after LeanerCloud/cloud-commitments-cli#1226 landed). Recording as evidence; the issue as written is accurate and the analysis stands.Where, on current line numbers:
internal/api/handler_auth.go:114-163;resolveAuthenticatedUserIDat:172-182.One detail worth adding to the fix. The plumbing needed to distinguish the two callers already exists and is simply not read here:
Session.UserAPIKeyID(internal/api/types.go:285) was threaded specifically so constraint checks can tell an API-key principal from its owning user, and it is consulted for that purpose atinternal/api/handler.go:492-501.getCurrentUserPermissionsignores it and goes straight toGetUserPermissionsAPI(principal.UserID). So the branch condition in the proposed fix does not need a new lookup;session.UserAPIKeyID != nil(or the equivalent principal check) is already available at the handler.Failure scenario as re-derived: a CI automation key deliberately scoped to
view:recommendationsonly, owned by an admin, calls/api/auth/me/permissionsand receives{"permissions":[{"action":"admin","resource":"*"}],"is_admin":true}. The frontend gates every purchase and approve affordance on this response, so it renders a full-admin surface for a read-only key. Agreeing with this issue's own framing: per-endpoint enforcement still holds, so this is disclosure plus a misleading capability advertisement rather than a direct bypass. It also tells the key's holder exactly which of the owner's privileges are worth trying.Related: #61 (unscoped user API key inherits owner's full permissions) is the enforcement-side sibling; this one is the advertisement side. Both should land with tests that assert the same effective set is reported and enforced, so the two cannot drift apart again.
IAmJSD commentedon Oct 16, 2026
Claiming: working on this
fix(api/auth): report a user API key's effective permissions on /me/p…