Repository navigation
sec(frontend): refuse to save a group with zero permissions - #387
Conversation
The group edit form's collectPermissions() skips any row whose action or resource is still the "Select Action"/"Select Resource" placeholder, so removing every real permission row (or leaving only a placeholder row) collected as permissions: []. The update API's documented contract (applyUpdateGroupRequest, internal/auth/service_api.go) treats an empty permissions list as "not sent" and leaves the group's stored permissions untouched -- there is no way for this endpoint to express "clear all permissions". So the form showed "Group updated successfully" and reloaded a group that still carried its old privileges, possibly admin:*. Checked the backend: this "empty means unchanged" behavior is an existing, documented, intentional contract of the update endpoint (not a bug to fix there); the gap is purely that the frontend form let an operator submit a payload that silently meant something other than what the UI told them they were doing. Refuse to submit when collectPermissions() returns an empty list, for both the create and update paths, with an explicit message directing the operator to delete the group instead. Also fixed two pre-existing tests whose assertions encoded the buggy behavior as correct (submitting permissions: [] and expecting it to reach the API). Co-Authored-By: claude-flow <ruv@ruv.net>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (4)
Included review availability: This review used your included allowance. 1 included review remains after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour. 📝 WalkthroughWalkthroughThe group form rejects submissions when it collects no permissions. The update API rejects an explicitly empty permissions list and treats omitted permissions as unchanged. Tests cover empty submissions and valid permission updates. ChangesGroup permission validation
Priority: ⬆️ High Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix · Severity of issue fixed: High Merge Risk: ⚪ Minimal · up to Empty permission lists are refused rather than reported as successful updates, while updates that omit permissions remain supported. No actionable merge-blocking risk is established. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
Independent review of #387 found the frontend guard alone was insufficient: any non-browser client (CLI, scripts, curl) could still PUT permissions: [] directly. UpdateGroupAPI's existing "empty means not sent" contract silently kept the group's old permissions (possibly admin:*) while returning 200, exactly the false-success scenario issue #237 describes for the UI path. encoding/json already distinguishes an omitted permissions field (decodes to nil) from an explicit `"permissions": []` (decodes to a non-nil, zero-length slice), so no API type change is needed. Add a presence check in UpdateGroupAPI: a non-nil-but-empty Permissions is now rejected with the new ErrEmptyPermissions sentinel, mapped to 400 by mapGroupAuthError. An omitted field still leaves permissions unchanged, unaffected. Co-Authored-By: claude-flow <ruv@ruv.net>
|
Independent adversarial review (two rounds) + local verification: MERGE at 2af296d. The UI refuses to save a group with zero permissions, and the backend now returns 400 for an explicit "permissions": [] while absent or null still means unchanged (proven through the real decode path with a throwaway test); both directions are pinned by mutation-checked tests. Follow-up (in #394's spirit): add a handler-level test pinning the 400 mapping, and fix the stale 'pointer' wording in errors.go/test comments. |
Summary
The group edit form skips permission rows with an empty action or resource, so removing every row and saving submitted an empty permission list. The backend's update contract treats an empty list as "not sent" and leaves stored permissions untouched, so the UI showed "Group updated successfully" and reloaded a group that still carried its old privileges, possibly
admin:*. An admin who believed they had stripped a group's privileges had not.Update: an independent review of this PR found the frontend-only guard insufficient -- any non-browser client (CLI, scripts, curl) could still send
PUT /api/groups/{id}with"permissions": []directly and hit the same silent no-op. This PR now fixes both layers.Root cause
frontend/src/groups/groupModals.ts'scollectPermissions()(line ~405) skips any row whose action or resource is still the empty "Select Action"/"Select Resource" placeholder.saveGroup()submitted whatevercollectPermissions()returned without checking for the empty case.internal/auth/service_api.go'sUpdateGroupAPI/applyUpdateGroupRequest(if len(perms) > 0) treats an empty permissions list as "field not sent" -- a deliberate, documented contract for the omitted-field case, but it also silently absorbed an explicit"permissions": [], which is indistinguishable at that point in the code from omission.Fix
Frontend:
saveGroup()now refuses to submit (for both create and update) whencollectPermissions()returns an empty list, showing: "A group must grant at least one permission. To remove all privileges, delete the group instead."Backend:
encoding/jsonalready distinguishes an omittedpermissionsfield (decodes tonil) from an explicit"permissions": [](decodes to a non-nil, zero-length slice) -- no API type change needed.UpdateGroupAPInow checksreq.Permissions != nil && len(req.Permissions) == 0and returns the newErrEmptyPermissionssentinel (mapped to HTTP 400 bymapGroupAuthError) for that explicit-empty case. An omitted field still leaves permissions unchanged, exactly as before.Regression tests
Frontend -- new
frontend/src/__tests__/group-edit-empty-permissions.test.ts:updateGroupand shows an error, not successAlso fixed two pre-existing tests whose assertions encoded the buggy behavior as correct:
groups.test.ts"should handle missing permissions list" expectedcreateGroupto be called withpermissions: []; updated to assert the save is now refused instead.group-edit-unrepresentable-permissions.test.ts"removing the offending row unblocks the save" removed the group's one and only permission row and expectedupdateGroupto be called withpermissions: []; added a second, unrelated clean permission so the test exercises the unrepresentable-constraint refusal in isolation from this new empty-permissions refusal.Backend -- new tests in
internal/auth/service_api_test.gounderTestService_UpdateGroupAPI:explicit empty permissions list is refused, not silently kept-- sendsPermissions: []APIPermission{}, assertsErrEmptyPermissions, and assertsUpdateGroup(the store write) is never called.omitted permissions field leaves the group's permissions unchanged-- negative control: omitted field still succeeds and leaves the group's stored permissions untouched.Proved pre-fix failure for both:
git stash push -- frontend/src/groups/groupModals.tsthen rangroup-edit-empty-permissions.test.ts-- 3 of 4 new tests failed (updateGroup/createGroupcalled once when it should not have been). Restored viagit stash popand reran: all 4 pass.TestService_UpdateGroupAPI/explicit_empty_permissions_list_is_refusedfailed with a mock panic ("I don't know what to return because the method call was unexpected" onUpdateGroup) -- i.e. the store write fired exactly as the issue describes. Restored the check and reran: all 6TestService_UpdateGroupAPIsubtests pass.Verification
npm test -- group-- 133 passed (3 suites)npm test -- users-- 2969 passed / 1 skipped (92 suites)npx eslint src/groups/groupModals.ts src/__tests__/group-edit-empty-permissions.test.ts src/__tests__/groups.test.ts src/__tests__/group-edit-unrepresentable-permissions.test.ts-- 0 errors (16 pre-existingno-explicit-anywarnings ingroups.test.ts, confirmed present onmainbefore this change, unrelated to this diff)npm run build-- webpack production build succeeds (pre-existing entrypoint-size warning only)GOTOOLCHAIN=go1.26.6 go build ./...-- cleanGOTOOLCHAIN=go1.26.6 go test -race -short ./internal/auth/...-- 801 passedGOTOOLCHAIN=go1.26.6 go test -race -short ./internal/api/...-- 2216 passedGOTOOLCHAIN=go1.26.6 go vet ./internal/auth/... ./internal/api/...-- cleanGOTOOLCHAIN=go1.26.6 go mod tidy -diff-- cleango install .../golangci-lint/v2/cmd/golangci-lint@v2.10.1) on./internal/auth/... ./internal/api/...-- 0 issuesCloses #237