Skip to content

feat(api/auth): flip mutating-route gate to handler-level requirePermission (PR-A of #660) - #726

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/660-router-permission-flips
May 27, 2026
Merged

feat(api/auth): flip mutating-route gate to handler-level requirePermission (PR-A of #660)#726
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/660-router-permission-flips

Conversation

@cristim

@cristim cristim commented May 25, 2026

Copy link
Copy Markdown
Member

Refs #660 (PR-A; PR-B frontend visibility + PR-C optional admin grant UI deferred per design comment).

What changed

The Permission{Action, Resource, Constraints} schema and HasPermission check have been in place for a while, plus handler-level requirePermission(action, resource) calls were wired into every mutating handler. The only chokepoint was internal/api/router.go — every mutating route had Auth: AuthAdmin, so non-admins got rejected at router.go:321 before the handler verb check ever fired.

This PR flips 14 mutating routes from AuthAdmin to AuthUser so handler-level requirePermission becomes the actual gate. Each was verified to already have its requirePermission call.

Routes flipped

  • Plans: POST /api/plans, POST /api/plans/{id}/purchases, PUT /api/plans/{id}/accounts, PUT /api/plans/{id}, PATCH /api/plans/{id}, DELETE /api/plans/{id}
  • Purchases: POST /api/purchases/execute, POST /api/purchases/planned/{id}/pause, POST /api/purchases/planned/{id}/resume, POST /api/purchases/planned/{id}/run, DELETE /api/purchases/planned/{id}
  • RI Exchange: POST /api/ri-exchange/quote, POST /api/ri-exchange/execute, PUT /api/ri-exchange/config

Defaults (internal/auth/types.go DefaultUserPermissions)

Added delete:plans and update:purchases (count 9 → 11). Other defaults unchanged.

Tests (+28 subtests across 7 test functions)

  • Per-route permission gates: with-permission / without-permission / admin-bypass
  • TestRouter_MutatingRoutes_RequireAuth — 7 routes, asserts unauthenticated callers still get 401 after the flip
  • Existing count assertions in auth/types_test.go, service_group_test.go, service_test.go bumped 9→11 (and 11→13 for user+groups)

Frontend

frontend/src/permissions.generated.ts auto-regenerated by the pre-commit hook to include the 2 new defaults in USER_PERMS.

1798 tests pass across internal/api and internal/auth.

Summary by CodeRabbit

  • New Features

    • Regular users can now delete plans and update purchases when granted the corresponding permissions.
    • Permission checks for plan, planned-purchase, and RI exchange actions are enforced at a more granular level.
  • Tests

    • Added and updated tests validating permission gates for mutating operations and ensuring correct 401/403 behavior for various sessions.

Review Change Stack

…ission (PR-A of #660)

Every mutating route in the plans, purchases, planned-purchases, and
ri-exchange groups was previously locked at AuthAdmin at the router
level, which meant the per-handler requirePermission(action, resource)
call was unreachable for non-admin users. This PR-A fix:

1. Router (internal/api/router.go): flips the Auth field on all
   mutating routes that already have a handler-level requirePermission
   call from AuthAdmin to AuthUser. The router-level gate now only
   asserts "must be signed in" (401 for anonymous callers); the real
   permission gate fires inside each handler.

   Routes flipped:
   - POST   /api/plans                    (create:plans)
   - POST   /api/plans/{id}/purchases     (create:plans)
   - PUT    /api/plans/{id}/accounts      (update:plans)
   - PUT    /api/plans/{id}               (update:plans)
   - PATCH  /api/plans/{id}               (update:plans)
   - DELETE /api/plans/{id}               (delete:plans)
   - POST   /api/purchases/execute        (execute:purchases)
   - POST   /api/purchases/planned/{id}/pause   (update:purchases)
   - POST   /api/purchases/planned/{id}/resume  (update:purchases)
   - POST   /api/purchases/planned/{id}/run     (execute:purchases)
   - DELETE /api/purchases/planned/{id}         (delete:purchases)
   - POST   /api/ri-exchange/quote        (view:purchases)
   - POST   /api/ri-exchange/execute      (execute:purchases)
   - PUT    /api/ri-exchange/config       (update:config)

2. Defaults (internal/auth/types.go): adds delete:plans and
   update:purchases to DefaultUserPermissions so regular users get
   the two most common operator verbs by default, per the design
   comment on #660.

PR-B (execute:purchases default grant) and PR-C (frontend) are deferred
per the design comment.

Refs #660 (PR-A; PR-B and PR-C deferred per design comment)
@cristim cristim added triaged Item has been triaged priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/feat New capability labels May 25, 2026
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12b940fb-02b0-46ac-bb89-a588b2f57ef1

📥 Commits

Reviewing files that changed from the base of the PR and between a47247e and 425ace4.

📒 Files selected for processing (1)
  • internal/api/router_660_permission_flips_test.go

📝 Walkthrough

Walkthrough

Baseline user permissions expand to include delete:plans and update:purchases. Router mutating routes for plans, purchases, and RI exchange switch from AuthAdmin to AuthUser (handler-level requirePermission enforces access). Tests and generated frontend permissions updated to match the new gating behavior.

Changes

Permission Definition and Router Enforcement Flip

Layer / File(s) Summary
Backend permission definition and baseline expansion
internal/auth/types.go, internal/auth/types_test.go, internal/auth/service_group_test.go, internal/auth/service_test.go
DefaultUserPermissions() adds delete:plans and update:purchases; backend tests updated to assert increased permission counts and include the new actions.
Frontend generated permissions synchronized
frontend/src/permissions.generated.ts
USER_PERMS regenerated to include delete:plans and update:purchases.
Router-level authentication gate flip to AuthUser
internal/api/router.go
Plans, planned-purchases, and RI-exchange mutating routes switched from AuthAdmin to AuthUser at registration; inline comments updated to document handler-level permission checks as the real gate.
Router permission-gate enforcement tests
internal/api/router_660_permission_flips_test.go
New Go test suite validating that non-admin users without required permissions get 403, users with the permission pass the gate (even if downstream errors occur), admins bypass checks, and unauthenticated requests return 401.
Frontend test expectations updated for user access
frontend/src/__tests__/permissions.test.ts, frontend/src/__tests__/plans-permissions.test.ts
Frontend tests adjusted to expect delete-plan and disable UI actions visible to the user role and to assert execute:purchases remains denied where applicable.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • LeanerCloud/CUDly#364: Modifies internal/api/router.go route-level auth gating and hardens router auth enforcement, closely related to this PR's mutating-route AuthAdmin → AuthUser flips.
  • LeanerCloud/CUDly#452: Updates frontend permission-gating tests and generated USER_PERMS, related to this PR's permission expansions and test adjustments.

Suggested labels

priority/p1, severity/high, type/security

Poem

🐰 I nibble at gates, flip locks with a twitch,
Users rise up, no longer stuck in a niche,
Tests hop along, permissions align,
Routes sing permission checks, crisp and fine,
A carrot for code — access now by design.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% 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 describes the main change: flipping mutating-route authorization from router-level (AuthAdmin) to handler-level (requirePermission). It is specific, concise, and clearly highlights the primary architectural shift.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/660-router-permission-flips

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

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 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.

…faults

Refs #660 (PR-A). Updates 4 test assertions in permissions.test.ts and
plans-permissions.test.ts to reflect the 2 new entries in
DefaultUserPermissions (delete:plans and update:purchases).

- getRolePermissions user-role test: expected array 9 -> 11 entries
- canAccess user-role denied test: delete:plans now toBe(true), not false
- plans-permissions user-role card test: delete-plan button now shown
- plans-permissions user-role row test: disable button now shown
@cristim

cristim commented May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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.

🧹 Nitpick comments (1)
internal/api/router_660_permission_flips_test.go (1)

82-96: ⚡ Quick win

assertNotForbidden is too permissive for “permission gate passed” assertions.

This helper treats any non-401/403 as success, so granted-path tests can pass even when execution fails before proving meaningful post-gate behavior. Consider adding a stronger signal per test (e.g., assert expected downstream call occurred, or assert HasPermissionAPI was actually invoked for non-admin paths).

🤖 Prompt for 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.

In `@internal/api/router_660_permission_flips_test.go` around lines 82 - 96, The
current assertNotForbidden helper (function assertNotForbidden) is too
permissive because it treats any non-401/403 ClientError as proof the permission
gate passed; change it to require an explicit positive signal that the post-gate
path executed: either (A) modify assertNotForbidden signature to accept an extra
boolean or interface (e.g., verified bool or a ctx/matcher) that callers set
when the downstream action or HasPermissionAPI mock was actually invoked, or (B)
make assertNotForbidden check a supplied mock flag/assertion method (e.g.,
validate that HasPermissionAPI was called on the permission mock) in addition to
using IsClientError/ce.code; update all callers to provide that signal so tests
fail if execution never reached the post-gate logic.
🤖 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.

Nitpick comments:
In `@internal/api/router_660_permission_flips_test.go`:
- Around line 82-96: The current assertNotForbidden helper (function
assertNotForbidden) is too permissive because it treats any non-401/403
ClientError as proof the permission gate passed; change it to require an
explicit positive signal that the post-gate path executed: either (A) modify
assertNotForbidden signature to accept an extra boolean or interface (e.g.,
verified bool or a ctx/matcher) that callers set when the downstream action or
HasPermissionAPI mock was actually invoked, or (B) make assertNotForbidden check
a supplied mock flag/assertion method (e.g., validate that HasPermissionAPI was
called on the permission mock) in addition to using IsClientError/ce.code;
update all callers to provide that signal so tests fail if execution never
reached the post-gate logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77243e1b-898b-42fe-a54f-a21d5d1896f3

📥 Commits

Reviewing files that changed from the base of the PR and between 5abf40b and a47247e.

📒 Files selected for processing (9)
  • frontend/src/__tests__/permissions.test.ts
  • frontend/src/__tests__/plans-permissions.test.ts
  • frontend/src/permissions.generated.ts
  • internal/api/router.go
  • internal/api/router_660_permission_flips_test.go
  • internal/auth/service_group_test.go
  • internal/auth/service_test.go
  • internal/auth/types.go
  • internal/auth/types_test.go

…ignal

Register t.Cleanup(func(){ m.AssertExpectations(t) }) in both authForUserWith
and authForAdmin so testify verifies every On() expectation was actually invoked.
This catches cases where the permission gate is bypassed before HasPermissionAPI
runs: the granted-path sub-test would have passed assertNotForbidden even if
the gate were never reached.

Also removes two incorrect GetAllowedAccountsAPI and one GetExecutionByID
expectation from admin-bypass sub-tests. Admin sessions short-circuit in
getAllowedAccounts (role == "admin" -> nil, nil) so IsUnrestrictedAccess returns
true and requirePlanAccess/requireExecutionAccess return nil immediately without
calling GetAllowedAccountsAPI or GetExecutionByID. Registering those expectations
with AssertExpectations enforced correctly surfaced them as bugs.
@cristim

cristim commented May 26, 2026

Copy link
Copy Markdown
Member Author

Addressed the assertNotForbidden nitpick (option B).

What changed (internal/api/router_660_permission_flips_test.go, commit 425ace4):

  • Added t.Cleanup(func() { m.AssertExpectations(t) }) inside both authForUserWith and authForAdmin. Testify now verifies every On() expectation was actually invoked, so a granted-path sub-test fails if HasPermissionAPI (or ValidateSession) was bypassed before being called.

  • Removed two incorrect GetAllowedAccountsAPI expectations and one GetExecutionByID expectation from the admin-bypass sub-tests. Admin sessions short-circuit in getAllowedAccounts (role == "admin" returns nil, nil without calling GetAllowedAccountsAPI), and requireExecutionAccess does the same via IsUnrestrictedAccess(nil) == true. Enforcing AssertExpectations correctly surfaced these as pre-existing bugs in the test setup.

All 1301 tests in ./internal/api/... pass.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

@cristim: The fix looks excellent. Using t.Cleanup(func() { m.AssertExpectations(t) }) directly inside the authForUserWith and authForAdmin helpers is the clean, idiomatic testify approach — it ties mock verification to the test lifetime automatically without requiring callers to remember to assert expectations. And removing those spurious GetAllowedAccountsAPI / GetExecutionByID expectations from the admin-bypass sub-tests is the correct fix, since the admin short-circuit path in getAllowedAccounts and IsUnrestrictedAccess means those methods are legitimately never reached. Enforcing AssertExpectations properly surfaced those as test setup bugs, which is exactly what you want.

Triggering a full review now.

✅ 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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 May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 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.

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/medium Moderate 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