Repository navigation
fix(auth): expose system_managed on API groups and guard UpdateGroup - #1513
Conversation
The dashboard hides Edit and Delete on a group whose system_managed flag is true (LeanerCloud#1392), but auth.APIGroup never carried the field, so the badge never showed. groupToAPIGroup now copies it as `system_managed` (omitted when false), matching frontend/src/api/types.ts. Service.UpdateGroup relied on its only caller, UpdateGroupAPI, to refuse a system-managed group. It now reads the stored row and refuses with ErrSystemManagedGroup (403 at the handler) itself, sharing the check with DeleteGroup via refuseSystemManagedGroup. Refs LeanerCloud#919 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rF6. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE Blocking:
Non-blocking:
Attacked:
Body-stale:
Reviewer: rF8. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths. |
|
VERDICT: APPROVE c18b648 PR #1513 (OPEN), head c18b648, one non-merge commit on origin/main. No migrations, no SQL changes. Findings:
No correctness, security or completeness findings. Attacked:
|
|
Local verification at |
|
Merged by IAmJSD's agent at IAmJSD's request (IAmJSD is acting owner while the maintainers are away), under the bar IAmJSD set: 3 approving independent review(s) on |
|
Independent review 2 (claude-fleet, cold): APPROVE c18b648 Findings:
Attacked:
Verified locally (GOTOOLCHAIN=go1.26.10, no creds, heavy runs under the lock):
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
#1392 hid Edit and Delete on system-managed group cards, but the cards never changed in production because the API never sent
system_managed:auth.APIGrouphad no such field andgroupToAPIGroupdid not copy it. Separately,Service.UpdateGrouphad no guard of its own. It relied on its only caller,UpdateGroupAPI, to checkSystemManagedfirst, which is what the issue flags.Closes #919
Root cause and fix
internal/auth/service_api.go:APIGroupgainsSystemManaged bool `json:"system_managed,omitempty"`andgroupToAPIGroupcopies it. The field name matches what the frontend reads (frontend/src/api/types.tsAPIGroup.system_managed, andisSystemManagedGroupinfrontend/src/groups/systemGroups.ts, which checks=== true).omitemptykeeps every custom group's JSON byte-identical to before, so the chore(api): replace any with concrete request types in AuthServiceInterface #169 wire-format pins don't move.internal/auth/service_group.go:Service.UpdateGroupnow loads the stored row and refuses a system-managed group withErrSystemManagedGroup, or a missing group withgroup not found, before writing.mapGroupAuthErroralready turns that error into a 403 at the handler. The check moved into a shared helper,refuseSystemManagedGroup, whichDeleteGroupnow uses too (its behavior is unchanged; it already had the same guard inline).group.SystemManaged, because the caller builds the*Groupitself and may leave the flag out. The new test passes such a struct.internal/api/openapi.yaml: documentssystem_managedon theGroupschema.How #919 is now covered
system_managedis true, and say whygroups-system-managed-919.test.ts)groupActions.ts)system_managed, so the #1392 badge actually appearsService.UpdateGroupConcurrency
The check reads the row and then writes it, so I looked at whether a concurrent request could slip in between. None can. No runtime path ever writes
groups.system_managed:PostgresStore.CreateGroupdoesn't insert the column andPostgresStore.UpdateGroupdoesn't set it. Only migrations change it, so no concurrent request can flip the flag between the read and the write. If another request deletes the group in that gap, the store'sRowsAffected() == 0path still returnsgroup not found.Cost
UpdateGroupAPIalready loads the group for its own guard, so an API update now reads the group twice. It's one extra primary-key lookup on a rare admin action. I keptUpdateGroupAPIcalling the guardedService.UpdateGroupso that removing either guard alone still leaves the other in place. Four existing mock tests that pinned a singleGetGroupcall now expect two (service_api_test.go,service_api_fields_test.go,server/adapter_test.go), andTestService_UpdateGroupstubs the new read.Verification
Unit, mock and local-Postgres evidence on branch head
c18b648e, which sits on main3bfc2fe5. Run withAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true.New tests:
internal/auth/group_system_managed_919_test.go, unit with a mock store:TestServiceUpdateGroup_SystemManagedGuard: a seeded group is refused even when the caller's struct hasSystemManaged: false; a missing group returns not-found without a write; an ordinary group still updates.TestAPIGroup_CarriesSystemManaged:GetGroupAPIandListGroupsAPIJSON carries"system_managed": truefor a seeded group and omits the key for a custom group.internal/auth/group_system_managed_db_test.go(integrationtag, real migrated Postgres):TestIntegration_SystemManagedGroup_APIFlagAndUpdateGuard:ListGroupsAPIflags Administrators and Purchaser.Service.UpdateGroupon the Purchaser row, given a flagless struct that renames it and wipes its permissions, returnsErrSystemManagedGroup, and the stored name, permissions andupdated_atare unchanged.Fail before, pass after:
service_group.goandservice_api.goreverted to main:go test -count=1 -run TestServiceUpdateGroup_SystemManagedGuard ./internal/auth/fails:An error is expected but got nil, then an unexpectedUpdateGroupmock call.go test -count=1 -run TestAPIGroup_CarriesSystemManaged ./internal/auth/fails on both subtests:system_managedis absent.go vet -tags integration ./internal/auth/fails to compile the integration test (APIGroup has no field SystemManaged).service_group.goreverted,go test -tags integration -count=1 -timeout 20m -run 'TestIntegration_SystemManagedGroup_APIFlagAndUpdateGuard' ./internal/auth/fails:Expected error with "system-managed group cannot be modified" in chain but got nil.-v:--- PASS (12.64s).Checks on the branch:
go build ./...,go vet ./...andgo vet -tags integration ./...are clean.go test -count=1passes for./internal/auth/,./internal/api/,./internal/server/and./internal/mocks/, run one package at a time.golangci-lintv2.10.1run --allow-parallel-runners ./internal/auth/ ./internal/server/and--build-tags integration ./internal/auth/both report 0 issues.gocyclo -over 10 -ignore "_test\.go" internal/auth/service_group.go internal/auth/service_api.goreports nothing.openapi.yamlparses, and the new property is present.Combined-tree verification at local queue commit
2be43e1e= origin/mainb9271e88+fix/issue-363-next,fix/issue-177-next,fix/issue-919-next,fix/issue-313-next,fix/issue-532-nextandfix/issue-316-nextmerged in that order. Passed:go build ./...;go vetwith and without-tags integration;go test -race -short -count=1 ./...; golangci-lint v2.10.1 with and without the integration tag; gocyclo -over 10;go test -race -tags integrationoninternal/api,internal/auth,internal/config,internal/database/postgres/migrations,internal/purchase,internal/serverandinternal/testutil; the git-secrets scan andscripts/test-git-secrets-allowlist.sh.Out of scope
system_managed(fix(frontend): hide Edit and Delete on system-managed groups #1392) and will start showing the "System group" badge as soon as this deploys.UpdateGroupAPIandService.UpdateGroup.PostgresStore.UpdateGroupandDeleteGroupthemselves still don't filter onsystem_managedin SQL. The service is the guard layer, and no write path can set the flag, so I left the store unchanged.Maintainer: please mirror the issue's triage labels onto this PR:
triaged,priority/p3,severity/low,urgency/this-quarter,impact/few,effort/xs,type/bug.🤖 Generated with Claude Code