Repository navigation
chore(api): type AuthServiceInterface user and group writes with the auth.API* request and response types - #1261
Conversation
…auth.API* request and response types CreateUserAPI, UpdateUserAPI, CreateGroupAPI and UpdateGroupAPI took and returned any, and each service implementation opened with a type assertion that failed with "invalid request type" at runtime. The import cycle that once justified any is gone (api already imports auth), so the interface, auth.Service, the server adapter and every test double now use the concrete request types and *APICreateUserResponse / *APIUser / *APIGroup responses. A wrong request type is now a compile error. No behavior change: the handlers decode into the same structs and return the same values to the same JSON encoder. A new HandleRequest-level test pins the four endpoints' request values and response bytes; it passes unchanged on both sides of this commit. The three adapter tests that passed maps (and so only ever hit the type-assertion error) now send typed requests and assert the store write and response. Closes LeanerCloud#169 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 14 billable files and costs up to $3.50. Or wait 59 minutes for your next included review. View limit details
Comment |
Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run. 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: rB10. 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: rB13. 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. |
|
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: 2 approving independent review(s) on |
TestUserGroupWrites_WireFormat came in from main (LeanerCloud#1261) and builds a Handler with no config store. Since LeanerCloud#1034, group writes bind every allowed_accounts entry to a registered account and refuse the write when the account list cannot be loaded, so POST /api/groups answered 500. The test now registers its account ID in a store mock, as the binding tests do; the wire format it pins is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(auth): bind group account scope entries to account IDs A group's allowed_accounts could hold an account's display name. The name was not bound to any account, so once that account was deleted or renamed the entry matched whichever account was later created or renamed to the same name. #1032 limited name matching to unique names, which does not help here: the newcomer is the only account carrying the name. Scope entries now match by account identifier only: - AccountScope.Allows never compares an entry with a display name. A caller that knows an account only by name (legacy rows keyed by external ID) still resolves through WithRegisteredAccounts to the one account carrying that name, and that account's ID must be in the scope. - Group create/update bind allowed_accounts at grant time: "*", account IDs and external IDs are kept; a name exactly one registered account carries, inside the actor's own scope, is stored as that account's ID; anything else is a 400. A failed account or scope load refuses the write. - Migration 000135 rewrites existing name entries to IDs when exactly one account carries the name and it predates the group's last write. Other entries are left in place and no longer grant access. Down is a no-op. - The name-only MatchesAccount and AuthContext.CanAccessAccount helpers, which had no production callers, are removed. Closes #1034 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(api): scope the history-summary fixture by account ID The history-summary integration tests from #1200 scoped their user with the tenant's display name. This branch binds allowed_accounts entries to account IDs and makes a bare name entry match nothing (#1034), so the scoped user saw no tenant A rows and the totals and listed-rows checks failed. Grant the scope by tenant A's account ID, the form group writes now store. The assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(db): renumber the group scope binding migration to 000149 main now ships migrations up to 000139, so 000135 would sit below versions a deployed database has already applied and golang-migrate would never run it. Open PRs hold 000140-000148, so this takes 000149 and must merge after that chain. The SQL is unchanged; only the file names, header comments, test version and test fixture IDs move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(db): renumber bind_group_account_names_to_ids migration 000149 -> 000145 golang-migrate only applies versions above the database's current one, so PRs must merge in migration-number order. 000140-000144 had been taken by PRs that are now held on owner decisions (#1239, #1241, #1253). The ready PRs move to 000140-000145 so they can merge ahead of them, and the held ones will take the next free numbers when decided. Rename only: the SQL statements are unchanged, and the test version constants and the comments naming the migration follow the new number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(api): register the wire-format group account for grant-time binding TestUserGroupWrites_WireFormat came in from main (#1261) and builds a Handler with no config store. Since #1034, group writes bind every allowed_accounts entry to a registered account and refuse the write when the account list cannot be loaded, so POST /api/groups answered 500. The test now registers its account ID in a store mock, as the binding tests do; the wire format it pins is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(api): scope the History paging user by account ID, not name TestIntegration_HistoryPaging_ScopedUserNeverPagesIntoAnotherTenant came in from main (#1277) and grants the scoped user tenant A by its display name. Since #1034 an allowed_accounts entry matches by cloud account ID only, so that user saw none of tenant A's rows and the walk returned only its own unscoped execution. The grant now names tenant A's ID, as the other scope tests on this branch do; what the test asserts is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(analytics): scope account_ids fixtures by account ID The account_ids analytics tests that arrived with the main merge (#1448) grant scope by account name ("a", "b", "c"). This branch binds group scope entries to account IDs and no longer matches an entry against a display name (#1034), so those name entries authorize nothing and the scoped cases got 404. List the account IDs in the scope instead, as the other analytics scope tests on this branch already do. The assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(analytics): scope the account_ids DB test by account ID TestAnalyticsAccountIDs_SumsSelectedAccountsOnly_DB (from #1448) grants the scoped user ["a", "b"], which are account names. Scope entries are account IDs on this branch (#1034), so the name entries allowed nothing and the in-scope query returned 404. Use the created accounts' IDs. The foreign-id refusal assertion is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(history): scope the plan_id filter DB test by account ID TestIntegration_HistoryPlanFilter_CrossTenant (from #1445) grants the scoped user tenant A by account name. Scope entries are account IDs on this branch (#1034), so the name allowed nothing and tenant A's own plan came back empty. Grant tenant A's ID, as the other history integration tests on this branch do. The cross-tenant assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(api): scope #1464's actionable History tests by account ID #1464 landed on main with tests that scope the user by the display name "dev" and with an executionScope case built on WithRegisteredNames. This branch (#1034) makes name entries grant nothing and replaces that constructor, so after merging main those tests no longer compiled or no longer exercised the scoped path. Scope them by the account's ID, which is what a group stores now, and pin that a name entry contributes only its literal value to the SQL scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth,api): bind account names to the identifier in scope checks Review (rE7) found that an ID-scoped user could pass the purchase gate for an account outside their scope. resolveAccountNamesByID keyed names by both account ID and the free-text external ID in one map, so an editor of in-scope account Z who set Z's external ID to victim Y's ID got Z's name back for Y. AccountScope.Allows then resolved that name to Z, which is in scope, and allowed a purchase in Y. The same map fed the dashboard, ladder and RI reshape filters. Decide those gates on the record's cloud_account_id itself, the ID the purchase resolves its credentials from, and delete the dual-keyed map. Bind the name path in Allows as well, so a name only identifies an account through that account's own external ID and never through a value that is some registered account's ID. Callers that pass a name for an external ID already resolve it within the record's provider or require exactly one owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Root cause
AuthServiceInterface(internal/api/types.go) declared the four user and group write methods withanyrequest and response types:Each
auth.Serviceimplementation started with a type assertion that returnedfmt.Errorf("invalid request type")on a mismatch. A caller passing the wrong type therefore failed at runtime (as a 500) instead of at compile time. The comment ininternal/auth/service_api.gosaidanywas there "to avoid import cycles with the api package". That reason no longer holds:internal/apialready importsinternal/auth(auth.PermissionConstraints,auth.APIPermissionand others appear in the same interface), so the interface can name theauth.API*types directly. No types need to move.Fix
Signatures only; no behavior change.
internal/api/types.go: the four methods now takeauth.APICreateUserRequest/auth.APIUpdateUserRequest/auth.APICreateGroupRequest/auth.APIUpdateGroupRequest. They return*auth.APICreateUserResponse/*auth.APIUser/*auth.APIGroup/*auth.APIGroup. These are exactly the dynamic types the service already returned.internal/auth/service_api.go: the implementations take the concrete types, the four type assertions and their"invalid request type"errors are gone, and the stale import-cycle comment is corrected.internal/server/app.go: theauthServiceAdaptersignatures are updated. The bodies are unchanged pass-throughs.MockAuthService(mocks_test.go),mockAuthForExchange(handler_ri_exchange_test.go),realGroupAuth(group_explicit_zero_cap_test.go) andadminUserUpdateAuth(handler_users_update_race_integration_test.go, integration tag).The handlers did not change. They already decode into these structs and pass the result straight to the JSON encoder, so the encoder receives the same values as before.
Test changes that fall out of the type change
MockAuthServicenow type-asserts its canned return values, the same way it does forGetUser. Several handler tests returnedmap[...]fixtures, which can no longer satisfy the interface. They now return the realauth.API*types.TestHandler_createUser_Successhad also been returning*auth.APIUserwhere the real service returns*auth.APICreateUserResponse; the stronger mock types caught that drift.internal/auth/service_api_test.go: the four"invalid request type"subtests are removed because that code path is now a compile error. Theresult.(*T)assertions become direct field access.internal/server/adapter_test.go:TestAuthServiceAdapter_CreateUserAPI/_CreateGroupAPI/_UpdateGroupAPIpassedmap[string]interface{}. They only ever reached the type-assertion error, and they ended in_ = err. They now send typed requests and assert the store write and the returned value.Wire format: proved byte-identical
New
internal/api/user_group_wire_format_test.go:TestUserGroupWrites_WireFormatdrivesHandler.HandleRequest(body decode → router → handler → interface →json.Marshal) forPOST /api/users,PUT /api/users/{id},POST /api/groupsandPUT /api/groups/{id}. For each request it asserts:max_amountmarker thatAPIPermissionConstraint.UnmarshalJSONsets (fix(frontend): a max_amount spend cap of 0 is silently erased when a group is edited #133), and the non-nil emptyallowed_accountsthat means "clear the restriction".main.The test file is identical before and after the refactor: sha256
b91f2ee923024cbb16ebe5d936a12c1dc8aaac2382ad6d84705bf5039086fb58on both sides, and it passes on both.TestAuthServiceInterface_UserGroupWritesAreTypeduses reflection to pin the four methods' request and response types. It is the fail-before/pass-after guard against a regression back toany.Typed-nil note: the real service returned a typed
*APIUser/*APIGroupinsideanybefore this change too, so even a hypothetical nil-pointer success would encode the same (null) on both sides. Only the old mock could return an untypednil.Closes #169
Verification
Unit, mock and fixture evidence on this branch (
7d75eb53). The env for every command wasAWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true.Fail-before, on unmodified
mainwith only the new test file added:Pass-after, on this branch with the same test file (same sha256):
Other checks:
No DB, migration or concurrency change, so no new race test is needed.
Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree:
queue/platform@936996ad= origin/maina14dc8f2plus these heads, merged in this order:fix/issue-72andfix/issue-312conflict on one line ininternal/api/handler_dashboard.go(both change thecalculateCommitmentMetricscall). The tree used #72's side with #312's validatedprovidervariable; whichever PR merges second takes that same resolution.Linux, Go 1.26.9,
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:936996ad:go build ./...,go vet ./...,go vet -tags integration ./...and the fullgo test -race -short -count=1 ./....936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without--build-tags integration.tests/e2eis a separate module, so it was vetted under its owne2etag.gocyclo -over 10is silent on the changed non-test files.-tags integration -count=1, 60-minute local timeout because the VM was shared):936996ad, these pass:internal/server,internal/credentials,internal/scheduler,internal/database/postgres/migrations,internal/purchaseandinternal/api.internal/configpasses exceptTestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).26795376, same branches at earlier heads):internal/analytics,internal/auth,internal/commitmentopts,internal/databaseandcmd/lambda/clear-rate-limitall pass. The later fixes don't touch those packages.httpToLambdaRequestsignature change;scripts/setup-git-secrets.shpatterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.Out of scope (deliberately left)
The issue scopes the four user and group write methods, and that is what this PR changes. Other
AuthServiceInterfacemethods still useanyand could get the same treatment in a follow-up:ListUsersAPI,GetGroupAPI,ListGroupsAPIandGetUserPermissionsAPI(response only).handler_auth.goandhandler_registrations.gostill type-assert the latter two results, and the comment there still cites the import cycle.CreateAPIKeyAPI(request and response),ListUserAPIKeysAPI,GetAPIKeysUsageStatsAPIandValidateUserAPIKeyAPI((any, any, error)), whichmiddleware.goandhandler_auth.gotype-assert.Labels
Could a maintainer please mirror the issue's triage labels onto this PR:
impact/internal,type/chore,triaged,priority/p3,severity/low,urgency/eventually,effort/m.🤖 Generated with Claude Code