Skip to content

chore(api): type AuthServiceInterface user and group writes with the auth.API* request and response types - #1261

Merged
IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-169
Oct 25, 2026
Merged

IAmJSD merged 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-169

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Root cause

AuthServiceInterface (internal/api/types.go) declared the four user and group write methods with any request and response types:

CreateUserAPI(ctx, actorUserID string, req any) (any, error)
UpdateUserAPI(ctx, actorUserID, userID string, req any) (any, error)
CreateGroupAPI(ctx, actorUserID string, req any) (any, error)
UpdateGroupAPI(ctx, actorUserID, groupID string, req any) (any, error)

Each auth.Service implementation started with a type assertion that returned fmt.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 in internal/auth/service_api.go said any was there "to avoid import cycles with the api package". That reason no longer holds: internal/api already imports internal/auth (auth.PermissionConstraints, auth.APIPermission and others appear in the same interface), so the interface can name the auth.API* types directly. No types need to move.

Fix

Signatures only; no behavior change.

  • internal/api/types.go: the four methods now take auth.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: the authServiceAdapter signatures are updated. The bodies are unchanged pass-throughs.
  • Test doubles updated: MockAuthService (mocks_test.go), mockAuthForExchange (handler_ri_exchange_test.go), realGroupAuth (group_explicit_zero_cap_test.go) and adminUserUpdateAuth (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

  • MockAuthService now type-asserts its canned return values, the same way it does for GetUser. Several handler tests returned map[...] fixtures, which can no longer satisfy the interface. They now return the real auth.API* types. TestHandler_createUser_Success had also been returning *auth.APIUser where 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. The result.(*T) assertions become direct field access.
  • internal/server/adapter_test.go: TestAuthServiceAdapter_CreateUserAPI / _CreateGroupAPI / _UpdateGroupAPI passed map[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_WireFormat drives Handler.HandleRequest (body decode → router → handler → interface → json.Marshal) for POST /api/users, PUT /api/users/{id}, POST /api/groups and PUT /api/groups/{id}. For each request it asserts:

    The test file is identical before and after the refactor: sha256 b91f2ee923024cbb16ebe5d936a12c1dc8aaac2382ad6d84705bf5039086fb58 on both sides, and it passes on both.

  • TestAuthServiceInterface_UserGroupWritesAreTyped uses reflection to pin the four methods' request and response types. It is the fail-before/pass-after guard against a regression back to any.

Typed-nil note: the real service returned a typed *APIUser / *APIGroup inside any before 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 untyped nil.

Closes #169

Verification

Unit, mock and fixture evidence on this branch (7d75eb53). The env for every command was AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true.

Fail-before, on unmodified main with only the new test file added:

$ go test ./internal/api/ -run 'TestUserGroupWrites_WireFormat|TestAuthServiceInterface_UserGroupWritesAreTyped' -count=1 -v
--- PASS: TestUserGroupWrites_WireFormat (all 4 subtests PASS: wire format recorded on main)
    expected: "auth.APICreateUserRequest"   actual: "interface {}"
    expected: "*auth.APICreateUserResponse" actual: "interface {}"
    ... (same for UpdateUserAPI, CreateGroupAPI, UpdateGroupAPI)
--- FAIL: TestAuthServiceInterface_UserGroupWritesAreTyped

Pass-after, on this branch with the same test file (same sha256):

$ go test ./internal/api/ -run 'TestUserGroupWrites_WireFormat|TestAuthServiceInterface_UserGroupWritesAreTyped' -count=1 -v
--- PASS: TestUserGroupWrites_WireFormat (4/4 subtests)
--- PASS: TestAuthServiceInterface_UserGroupWritesAreTyped

Other checks:

go build ./...                                       # ok
go vet ./...                                         # clean
go vet -tags integration ./...                       # clean
go test -count=1 ./internal/api/                     # ok
go test -count=1 ./internal/auth/                    # ok
go test -count=1 ./internal/server/                  # ok
go test -count=1 ./internal/mocks/                   # ok
go test -tags integration -count=1 -timeout 20m -run 'TestAdminUpdateUserHTTPKeepsConcurrentMFA' ./internal/api/   # PASS (real Postgres via testcontainers; its fixture signature changed)
golangci-lint v2.10.1 run --allow-parallel-runners ./internal/api/... ./internal/auth/... ./internal/server/...   # 0 issues
golangci-lint v2.10.1 run --allow-parallel-runners --build-tags integration ./internal/api/...                     # 0 issues
gocyclo -over 10 -ignore "_test\.go" internal/api/types.go internal/auth/service_api.go internal/server/app.go     # none

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/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-337 ff3420c
fix/issue-169 7d75eb5
fix/issue-342 66b221f
fix/issue-310 5e11c21
fix/issue-326 5f0ea59
fix/issue-243 365e959
fix/issue-343 6c94036
fix/issue-312 d2848a8
fix/issue-316 acd60dd
fix/issue-339 75ea9b9
fix/issue-339-commitment-type 73bdb19
fix/issue-353 5d25fcd
fix/issue-313 3156bc4
fix/issue-357 53d9e58
fix/issue-368 0ba727d
fix/issue-419 61648c9
fix/issue-111 b04c372
fix/issue-686 0ad90b7
fix/issue-674 793095d
fix/issue-1055 4ec96f1
fix/issue-559 dfa9525
fix/issue-558 034eed4
fix/issue-874 1720539
fix/issue-666 1e120da
fix/issue-668 fb26345
fix/issue-568 ce04173
fix/issue-311 e54f250
fix/issue-177 0cbb88a
fix/issue-151 c21377c
fix/issue-166 41fb77b
fix/issue-27 2d67622
fix/issue-323 69288ae
fix/issue-59 36463e8
fix/issue-43 35e4069
fix/issue-158 6f76f8a
fix/issue-104 bb00491
fix/issue-55 971ef1b
fix/issue-361 89ac220
fix/issue-120 b458719
fix/issue-36 42bac6e
fix/issue-525 57c97dd
fix/issue-712 e5bcda0
fix/issue-72 6cc63ed

fix/issue-72 and fix/issue-312 conflict on one line in internal/api/handler_dashboard.go (both change the calculateCommitmentMetrics call). The tree used #72's side with #312's validated provider variable; 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:

  • Passed at 936996ad: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks at 936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. tests/e2e is a separate module, so it was vetted under its own e2e tag. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1, 60-minute local timeout because the VM was shared):
    • At 936996ad, these pass: internal/server, internal/credentials, internal/scheduler, internal/database/postgres/migrations, internal/purchase and internal/api.
    • internal/config passes except TestPostgresStoreDB_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).
    • These were run on the first build of this tree (26795376, same branches at earlier heads): internal/analytics, internal/auth, internal/commitmentopts, internal/database and cmd/lambda/clear-rate-limit all pass. The later fixes don't touch those packages.
  • What the combined run caught, each fixed on its own branch before this PR opened:
    • a compile break between fix/issue-104's test and fix/issue-326's httpToLambdaRequest signature change;
    • fix/issue-316 rejecting legacy-keyed plans;
    • fix/issue-120 versus main's global-filter test.
  • Secrets: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns 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.
  • Nothing was applied to any cloud.

Out of scope (deliberately left)

The issue scopes the four user and group write methods, and that is what this PR changes. Other AuthServiceInterface methods still use any and could get the same treatment in a follow-up:

  • ListUsersAPI, GetGroupAPI, ListGroupsAPI and GetUserPermissionsAPI (response only). handler_auth.go and handler_registrations.go still type-assert the latter two results, and the comment there still cites the import cycle.
  • CreateAPIKeyAPI (request and response), ListUserAPIKeysAPI, GetAPIKeysUsageStatsAPI and ValidateUserAPIKeyAPI ((any, any, error)), which middleware.go and handler_auth.go type-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

…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>
@IAmJSD IAmJSD added impact/internal Team-internal only type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline effort/m Days labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 14 billable files and costs up to $3.50.

Or wait 59 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 503eb2e and 683e329.

Files selected for processing: 14
  • internal/api/group_explicit_zero_cap_test.go
  • internal/api/handler_coverage_test.go
  • internal/api/handler_groups_ceiling_test.go
  • internal/api/handler_groups_test.go
  • internal/api/handler_ri_exchange_test.go
  • internal/api/handler_users_test.go
  • internal/api/handler_users_update_race_integration_test.go
  • internal/api/mocks_test.go
  • internal/api/types.go
  • internal/api/user_group_wire_format_test.go
  • internal/auth/service_api.go
  • internal/auth/service_api_test.go
  • internal/server/adapter_test.go
  • internal/server/app.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 683e3295caa32a3ab03e79cab4994d1c6f72d110

Blocking:

  • none

Non-blocking:

  • internal/auth/service_api.go:238-241: the new comment says the methods "that still use any predate that and are not yet converted" (ListUsersAPI, GetGroupAPI, ListGroupsAPI and others). Fine to leave, but a follow-up issue would keep them from staying any indefinitely, since the reason for using any no longer applies.

Attacked:

  • Auth-path equivalence: the four handlers (createUser, updateUser, createGroup, updateGroup) didn't change. They still decode into the same auth.API* structs, call the method with session.UserID as the actor, and map errors through mapAuthError/mapGroupAuthError. The service bodies differ only in losing the type assertion. The grant-ceiling, account-ceiling, system-managed and self-escalation checks are untouched. I checked for a typed-nil change in behavior: every success return in the four service methods is a freshly built non-nil pointer, and every error return is nil, err, so the any the handler returns encodes exactly as before. The only non-test implementors are auth.Service and server.authServiceAdapter, and both are pure pass-throughs.
  • Fail-before: I put internal/api/user_group_wire_format_test.go on an unmodified origin/main worktree. TestUserGroupWrites_WireFormat passed 4/4 there, which shows the wire bytes were recorded against main. TestAuthServiceInterface_UserGroupWritesAreTyped failed (expected "auth.APICreateUserRequest" actual "interface {}" and so on). On the head both pass.
  • Ran go build ./..., go vet on api/auth/server with and without -tags integration, tests/e2e go vet -tags e2e, go test -race -short -count=1 on api/auth/server, and go test -race -tags integration on api (including TestAdminUpdateUserHTTPKeepsConcurrentMFA, whose fixture changed) and on auth's API and Group tests. All ok. golangci-lint v2.10.1 found 0 issues with and without --build-tags integration.

Body-stale:

  • none

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.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 683e3295caa32a3ab03e79cab4994d1c6f72d110

Blocking:

  • none

Non-blocking:

  • internal/api/mocks_test.go:253-326: the typed MockAuthService methods panic, rather than fail cleanly, if a future test passes the wrong fixture type to .Return(...). That's the same pattern GetUser already uses and is acceptable for a test double. Noting it only because it moves the old silent drift (the *auth.APIUser vs *auth.APICreateUserResponse mismatch this PR found) into a panic, which is the right direction.
  • The verification section cites 7d75eb53. The head is now 683e3295 (after a main merge); see below for what I re-ran.

Attacked:

  • Auth-path behavior: the four service methods differ from main only by dropping the reqInterface.(T) assertion. The handlers (handler_users.go:70,143, handler_groups.go:53,115) already passed auth.API*Request values and return the pointer as any, so the JSON encoder gets the same dynamic type as before. No service path returns (nil, nil), so there's no typed-nil change, and the actor ID, ceiling and self-escalation arguments are unchanged. authServiceAdapter stays a pure pass-through. Every implementer (service, adapter, three test doubles plus the integration-tagged one) compiles under both tag sets.
  • Fail-before: checked out the merge-base for every changed file except the new user_group_wire_format_test.go. TestUserGroupWrites_WireFormat PASSES there (4/4: the wire format recorded on the old code), and TestAuthServiceInterface_UserGroupWritesAreTyped FAILS (actual: "interface {}"). Restored HEAD, where both pass. This confirms the body's sha-identical-test claim.
  • Semantic-merge check: did a scratch git merge --no-commit origin/main (68 commits ahead of the merge-base, including fix(api): report unrestricted account access on /auth/me/permissions #1155/fix(api): reject an explicit max_amount of 0 on API-key create #1406 in api/auth). go build ./..., go vet with and without -tags integration, and go test -short on api/auth/server all pass. Aborted afterwards.
  • On the head: go build ./...; go vet on api/auth/server with and without -tags integration; tests/e2e go vet -tags e2e; go test -race -short -count=1 on api/auth/server (ok); go test -race -tags integration -count=1 on ./internal/api/ (188s), ./internal/server/ and ./internal/auth/ (all ok); golangci-lint with and without --build-tags integration (0 issues).

Body-stale:

  • none material. The verification heads (7d75eb53, combined tree 936996ad) predate the current head 683e3295, but the re-run above covers it.

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.

@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at 683e3295caa32a3ab03e79cab4994d1c6f72d110 merged onto main (5f717aa4), together with #1260, #1261, #1263, #1264, #1269, #1271, #1273, #1308, #1416 in that order (combined tree 62127c84, Linux, Go 1.26.10): go build ./..., go vet with and without -tags integration, the full go test -race -short -count=1 ./..., and go test -race -tags integration -count=1 on every changed package, against Docker Postgres. All pass. CI Success and pre-commit are green on this head, with 2 approving independent review(s) on it. Nothing was run against any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 25, 2026 21:53
@IAmJSD
IAmJSD merged commit 0701eaf into LeanerCloud:main Oct 25, 2026
34 checks passed
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

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 683e3295 (two required: money, auth or security path), green CI, and the combined-tree verification above. Squash-merged at exactly 683e3295caa32a3ab03e79cab4994d1c6f72d110.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 26, 2026
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>
IAmJSD added a commit that referenced this pull request Oct 26, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(api): replace any with concrete request types in AuthServiceInterface

1 participant