Repository navigation
fix(auth): lock the default Standard and Read-Only user groups - #920
Conversation
Migration 000125 sets groups.system_managed = TRUE for the seeded Standard Users (...0005) and Read-Only Users (...0006) groups, so the system-managed guard (ErrSystemManagedGroup) refuses API edits and deletes of them like the other seeded groups. Owner decision 2026-10-16 on issue #168. Locking is by id: a group an admin already renamed is locked as-is, permissions untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@coderabbitai review |
|
Warning Review limit reached
This review includes 3 billable files and costs up to $0.75.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 40 minutes for your next included review. View limit details
Comment |
|
cristim
left a comment
There was a problem hiding this comment.
Independent review of 3fc16e2: no actionable findings
Findings: none actionable.
- nit (non-blocking): the lock is by id, so if 000057's
ON CONFLICT DO NOTHINGskipped the seed because a different-id group already owned the name, the UPDATE is a silent no-op for that id. The deployed-DB path is the normal case (ids exist), so I would not change it.
What I attacked
- Guard coverage: the only runtime writers to
groupsarePostgresStore.UpdateGroup(store_postgres.go:547) andDeleteGroup(:587) on origin/main. Their only service callers areUpdateGroupAPI(service_api.go:376, checksSystemManaged, then callsService.UpdateGroupat :401) andService.DeleteGroup(service_group.go:50). Renames and permission edits go through UpdateGroupAPI, deletes through DeleteGroup; both refuse withErrSystemManagedGroup, mapped to 403 at handler_groups.go:73.Service.UpdateGroup(service_group.go:30) has no guard of its own, but has no other caller (already noted in the PR body and #919). No startup seeder writes these rows. - Blocked legitimate work: membership lives in
users.group_ids, outside the guarded group-row paths, so adding or removing users in Standard Users still works. Editing or deleting these two groups is blocked by design (owner decision per PR body). - Tenancy:
groupsrows are global seeds, no tenant column. Ids ...0005 and ...0006 match 000057_drop_user_role_to_groups.up.sql:18 (Standard Users) and :35 (Read-Only Users). ...0007 is Purchaser (000064). - Down migration: 000059_seed_purchaser_group.up.sql:14-21 locked only ...0001 to ...0004 and its down only drops the column, so before 000125 neither id was locked; restoring FALSE is the correct inverse. Test asserts Purchaser stays locked after down.
- Ordering: origin/main tops out at 000124; no open PR other than #920 touches migrations 0001xx; #905 has no migration files; bus claim claim-20261016-claude-cc-impl-migration-000125.txt is #920's own. #631 is not resolvable as a PR number.
- Test: ran
go test -tags integration -race -count=1 -run TestMigration_LockDefaultUserGroupsagainst real Postgres 16 (testcontainers): PASS. Mutation probe: replaced the up SQL withSELECT 1;, test failed with "after 000125 ...0005 must be system-managed"; reverted. That is the fail-before proof.
Gate status
- Independent review: this comment, at the SHA above.
- Findings resolved: none open. CodeRabbit check is "rate limited", no threads posted.
- CI on SHA: at review time several checks (Unit Tests, Lint Code, Integration Tests, E2E, Build Docker Image) were still pending; the remaining listed checks passed. Merge must wait for them to go green.
- Local real-path evidence: present in the PR body (testcontainers Postgres, auth/api suites, lint); I reproduced the migration test. No real environment was touched.
- Labels: priority/p2, severity/medium, urgency/this-sprint, impact/many, effort/s, type/security, triaged present; I did not diff them against #168's labels.
|
Gate record for 3fc16e2 (origin/main CLAUDE.md "Review gate"):
Merging at 3fc16e2 with --match-head-commit. |
Summary
Closes #168.
The seeded Standard Users (
00000000-0000-5000-8000-000000000005) and Read-Only Users (...0006) groups were notsystem_managed, so the #1737 guard did not protect them. An admin could strip or widen their permissions. The owner decided on 2026-10-16 to lock them.Migration 000125 sets
system_managed = TRUEfor both ids. The down migration restoresFALSEfor them; neither group was locked before this migration, since 000059 only locked ...0001 to ...0004. Locking is by id, so a group an admin already renamed is locked with its name and permissions unchanged.After this change,
UpdateGroupAPIandDeleteGroupon either group returnErrSystemManagedGroup(HTTP 403), as they already do for Administrators and Purchaser. Group membership lives inusers.group_idsand does not go through the guarded paths. Admins can still add or remove users, and new users still default to Standard Users.Tests
000125_lock_default_user_groups_test.goruns against a real Postgres 16 via testcontainers (integrationtag). It does the following:Mutations of the migration, each of which made the test fail:
...0005 must be system-managed....0006 must be system-managed.down must restore ...0005.The guard itself is already covered generically by
internal/auth/group_system_managed_test.go(UpdateGroupAPIandDeleteGroupreturnErrSystemManagedGroup).Verification (local, macOS)
go test -tags integration -race -count=1 ./internal/database/postgres/migrations/: the full package passes, so no other migration test is affected.go test -race -count=1 ./internal/auth/... ./internal/api/...: 3997 passed.--build-tags integration: 0 issues.go vetis clean with and without the tag.groupsoutside migrations: only the guardedUpdateGroupAPIandDeleteGrouppaths. There is no startup seeder, and the e2e fixtures do not write these groups.Migration ordering
origin/main is at 000124. No open PR uses 000125, which is claimed on the agent bus.
Review
An independent non-author ran two passes at 3fc16e2 and found nothing blocking. Non-blocking notes:
Service.UpdateGrouphas no guard of its own. It relies on its only caller,UpdateGroupAPI, to apply it. This is also noted in fix(frontend): hide Edit and Delete on system-managed groups #919.🤖 Generated with Claude Code