Skip to content

fix(api): 404 for missing users/groups/API keys, 400 for bad read filters (#312) - #1267

Merged
IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-312
Oct 26, 2026
Merged

IAmJSD merged 4 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-312

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Works through the four low-severity internal/api findings bundled in #312 (2026-09-02 audit). I re-checked each one against current main (a14dc8f). Before touching a function, I diffed it against every in-flight branch: open PRs #1111, #1116, #1141, #1212, #1237, #1241, #1242, #1243, #1244, and the unopened fix/issue-532, fix/issue-54, fix/issue-169, fix/issue-310, fix/issue-337 on the IAmJSD fork.

Refs #312

This is a partial fix (hence Refs). A02-007 is deferred to #1141, and the A01-021 dashboard copy needs the UI owner.

Per-finding status

Finding Status on main This PR
A01-021: cancelling one upcoming purchase disables its whole plan, but the docs say the plan stays intact Still present. Since #774 (and the scope work in #520/#623) the cancel deliberately disables the plan and returns plan_disabled: true. The UpcomingPurchase Go doc and the dashboard confirm dialog still say the plan stays in place. Backend half fixed: the UpcomingPurchase contract comment in internal/api/types.go now describes the real behavior. This only changes a comment, so it has no fail-before test; the existing delete_planned_purchase_scope_test.go tests already pin the disable behavior. The UI copy is deferred to the UI owner (see "Frontend follow-up"). I kept the #774 behavior rather than dropping the disable, because #774/#520/#623 chose it on purpose. Reversing it is an owner decision.
A02-007: approveRegistration commits approved before the account insert Still present on main. Deferred to #1141 (fix/issue-1102). That branch rewrites exactly these lines of approveRegistration: it moves the transition, the account insert and the link into a single ApproveAccountRegistration transaction. Editing them here would conflict.
A02-012: a missing user, group or API key returns 500 Still present. GET /api/users/{id} gets the raw pgx.ErrNoRows; GET /api/groups/{id} gets raw pgx.ErrNoRows from scanGroup or a plain "group not found"; API key delete/revoke get plain-string errors. All of them become 500 plus an error-level log line, while openapi.yaml already documents 404. Fixed. See below.
A02-021: enumerated query params are not validated on the dashboard, inventory and accounts reads Still present (provider=AWS or enabled=1 → 200 with an empty or unfiltered result). Fixed. See below.

No in-flight branch edits the lines changed here. The nearest overlaps:

I put the new tests in new files so they can't conflict with branches that edit the existing test files.

Root cause and fix

A02-012. internal/auth had no not-found sentinels, and IsNotFoundError only matches the router's own type, so handleRequestError treated every not-found as a server failure.

  • internal/auth/errors.go adds four sentinels:
    • ErrUserNotFound and ErrGroupNotFound. These wrap pgx.ErrNoRows when the store returned it, so callers that check errors.Is(err, pgx.ErrNoRows) keep working.
    • ErrAPIKeyNotFound.
    • ErrAPIKeyNotOwned. Its message is unchanged: unauthorized: cannot <action> another user's API key.
  • These now return the sentinels: Service.GetUser, Service.GetGroupAPI, authorizeAPIKeyAccess (behind delete and revoke), and the server adapter's nil-user branch.
  • Handler mapping:
    • mapAuthError (used by getUser) maps ErrUserNotFound to 404 "user not found".
    • getGroup maps ErrGroupNotFound to 404 "group not found".
    • The new mapAPIKeyAccessError maps both ErrAPIKeyNotFound and ErrAPIKeyNotOwned to the same 404 "API key not found", so a non-admin can't tell another user's key ID from a nonexistent one.
    • Any other error still returns a 500.

A02-021. These handlers forwarded params["provider"] without validation. buildAccountFilter silently ignored any enabled value other than true/false.

  • The new providerFilterParam runs validateProvider and maps all to "no filter". validateProvider already accepts all as "every provider"; before this change these reads filtered on the literal string "all" and returned nothing.
  • getDashboardSummary, fetchCommitmentRecords (behind /api/inventory/commitments and /api/inventory/coverage) and getCoverageBreakdown now use it before any store read.
  • buildAccountFilter (behind GET /api/accounts and /api/accounts/list) now returns (filter, error). It rejects unknown providers and any enabled other than true/false/empty with 400.
  • openapi.yaml adds the 400 response to /api/dashboard/summary. The inventory and accounts list endpoints aren't in the spec.

The frontend only sends aws|azure|gcp|'' and enabled=true|false (frontend/src/api/{dashboard,inventory,accounts}.ts), so the UI is unaffected.

No DB or schema change and no new concurrency surface, so no race test applies. The store already returns pgx.ErrNoRows for a missing user, group or key (scanUser/scanGroup/scanAPIKey; store_postgres_test.go pins the group case), so the not-found paths are covered with mocks at the service and handler layers.

Verification

All of this is unit/mock evidence with AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, Go 1.26.9. No real cloud or HTTP traffic: the internal/api no-network guard was active.

Fail-before (run in TDD order: tests written first, then the fix):

  • go test -count=1 -run 'TestService_GetUser_MissingUserIsErrUserNotFound|TestService_GetGroupAPI_MissingGroupIsErrGroupNotFound|TestService_APIKeyAccess_NotFoundAndNotOwnedSentinels' ./internal/auth/. This ran with only the sentinel declarations added. Result: FAIL, Target error should be in err chain: expected: "user not found" (and the same for group, API key and unauthorized, 8 assertions).
  • go test -count=1 -run '^TestAuthServiceAdapter_GetUser_MissingUserIsErrUserNotFound$' ./internal/server/. This ran with app.go and service_user.go reverted to main. Result: FAIL in both subtests (store_returns_ErrNoRows, store_returns_nil_user).
  • go test -count=1 -run 'TestHandler_getUser_MissingUserIs404|TestHandler_getGroup_MissingGroupIs404|TestHandler_APIKeyDeleteRevoke_' ./internal/api/. Result: FAIL with:
    • must be a client error, not a 500: user not found: no rows in result set
    • ... group not found
    • ... failed to delete API key: API key not found
    • ... failed to revoke API key: unauthorized: cannot delete another user's API key
  • Each A02-021 test was run alone (-run '^<name>$' ./internal/api/) on the pre-fix handlers:
    • TestHandler_getDashboardSummary_RejectsUnknownProvider: FAIL. provider=AWS reached ListRecommendations (mock: I don't know what to return because the method call was unexpected).
    • TestHandler_getDashboardSummary_AllProviderMeansNoFilter: FAIL. ListRecommendations was called with Provider:"all".
    • TestHandler_inventory_RejectsUnknownProvider: FAIL. The store was read with provider=AWS.
    • TestHandler_listActiveCommitments_AllProviderMeansNoFilter: FAIL. "[]" should have 2 item(s), but has 0.
    • TestHandler_getCoverageBreakdown_AllProviderMeansNoFilter: FAIL. provider=all must read the same as no provider.
    • TestListAccounts_RejectsUnknownEnumParams: FAIL. An error is expected but got nil for enabled=1|yes|TRUE, provider=AWS|ibm|"aws ", on both /api/accounts and /api/accounts/list.
    • TestListAccounts_AcceptedFilterValues: FAIL. provider=all produced a non-nil provider filter.

Pass-after on the branch head:

  • All of the tests above pass, plus the "store failure stays 500" negative controls (TestHandler_getUser_StoreFailureStays500, TestHandler_getGroup_StoreFailureStays500, TestHandler_APIKeyDeleteRevoke_StoreFailureStays500).
  • Package runs, one at a time:
    • 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. This includes the vacuous-mock-assertion guard, which caught two matcher-less AssertNotCalled calls in my first draft; both are fixed.
  • go build ./..., go vet ./... and go vet -tags integration ./... → clean.
  • golangci-lint run --allow-parallel-runners ./internal/api/ ./internal/auth/ ./internal/server/ (v2.10.1) → 0 issues.
  • gocyclo -over 10 -ignore "_test\.go" on every changed non-test file → nothing over 10 (mapAuthError is 5, buildAccountFilter is 6).

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 / notes

  • Other plain-string "API key not found" errors remain in internal/auth/service_apikeys_api.go (around lines 425 and 474), on paths the audit didn't flag. I left them alone.
  • updateUser/deleteUser for a missing user weren't in the finding and keep their current behavior. mapAuthError now also maps ErrUserNotFound, but only getUser produces it today.
  • External API clients (CLI, MCP) that send a provider other than aws|azure|gcp|all, or enabled other than true|false, to these reads now get 400 instead of a silently empty or unfiltered 200. I could not check those repos from here.
  • When TestListAccounts_Success (an existing test) runs alone, the internal/api no-network guard reports blocked IMDS calls from markSelfAccount. The same happens on main. It passes in the full package run. Unrelated to this change; my new account tests pre-resolve the source identity so they never make that call.

Frontend follow-up for the UI owner

A01-021: frontend/src/dashboard.ts cancelScheduledPurchase still tells the user: "Cancelling removes this upcoming step from the plan. The plan itself stays in place; the next scheduled step (if any) is unaffected." The comment above it says the same. Since #774 the backend also disables the plan, so no further purchases happen until someone re-enables it on the Plans page. The response carries plan_disabled: true. Suggested change:

  • Reword the confirm body to say the plan will be disabled.
  • Optionally, use plan_disabled in the success toast, e.g. "Purchase cancelled; plan disabled".

If the owner would rather the cancel not disable the plan, that reverses #774/#520/#623 and should be a separate issue.

Issue #312 can close once #1141 (A02-007) and the UI copy change above have both landed.

Maintainers: please mirror the issue's triage labels onto this PR: triaged, priority/p3, severity/low, urgency/eventually, impact/few, effort/m, type/bug.

🤖 Generated with Claude Code

…ters

Three of the four low findings in the 2026-09-02 audit issue, re-verified
on current main:

- A02-012: GET /api/users/{id}, GET /api/groups/{id} and DELETE / revoke
  of an API key answered 500 for a missing row (raw pgx.ErrNoRows or an
  untyped error) where openapi.yaml documents 404. internal/auth now
  returns ErrUserNotFound, ErrGroupNotFound (both still match
  pgx.ErrNoRows), ErrAPIKeyNotFound and ErrAPIKeyNotOwned, and the
  handlers map them to 404. A non-admin acting on another user's key gets
  the same 404 as a missing key.
- A02-021: the dashboard summary, both inventory reads and both account
  lists accepted any provider (and any "enabled") and answered 200 with
  an empty or unfiltered result. They now validate with validateProvider
  (400, as on recommendations/history) and treat provider "all" as no
  filter; enabled must be true or false.
- A01-021: the UpcomingPurchase doc said canceling one upcoming purchase
  leaves its plan active, but since LeanerCloud#774 the cancel also disables the
  plan. The Go contract comment now says so; the dashboard confirm copy
  is left to the UI owner.

A02-007 (approveRegistration ordering) is left to the in-flight fix on
fix/issue-1102 (LeanerCloud#1141), which rewrites the same lines.

Refs LeanerCloud#312

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline impact/few Limited audience effort/m Days type/bug Defect labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • 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 26, 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 6ef4b55f03369c3565a3757a8ebb31c408a600bd

Blocking:

  • none

Non-blocking:

  • The PR is CONFLICTING with main: git merge-tree shows one conflict, in internal/api/handler_dashboard.go, where main changed the calculateCommitmentMetrics call. Use main's side with this PR's validated provider variable, as the body says. Merging main makes a new head, which needs a fresh review.
  • internal/auth/service_apikeys_api.go (~425, ~474) and internal/auth/service_group.go (79-84, 177-182) still return plain-string "API key not found" / "user not found" errors that end up as 500s. They're outside the audited lines and the body says so. They're cheap to convert to the new sentinels in a follow-up.
  • getCurrentUser (handler_auth.go:94) still passes a missing-user GetUser error straight through (500) for a session whose user was deleted. That's unchanged behavior, but mapAuthError now makes a 401/404 there easy.
  • API change for external clients: provider outside aws|azure|gcp|all|"" or enabled outside true|false|"" on the dashboard, inventory and accounts reads now returns 400. I grepped cloud-commitments-cli and cloud-commitments-mcp origin/main and found no callers of these endpoints. The frontend's Provider type is aws|azure|gcp, and it only sends enabled=String(bool).

Attacked:

  • Auth/enumeration. getUser/getGroup check view:users/view:groups before the lookup, so the new 404 tells nothing to a caller without the permission. Delete/revoke on another user's key (non-admin) and on a nonexistent key give the same 404 "API key not found", so no ownership oracle. Store failures still give 500 (the negative-control tests cover it).
  • Sentinel wrapping. ErrUserNotFound/ErrGroupNotFound wrap pgx.ErrNoRows with %w: %w. I checked every GetUser caller (lookupRequesterInfo, history email resolve, getCurrentUser, the server adapter): none matches on the old error text.
  • Filters. providerFilterParam runs before any store read in all 4 handlers. No raw params["provider"] is left in the dashboard, inventory or accounts handlers. all now means no filter, where before it filtered on the literal string and matched nothing.
  • Fail-before. In my scratch checkout I reverse-applied the PR's non-test hunks (keeping only the new sentinel declarations and helper signatures so the tests compile). These failed: TestService_GetUser_MissingUserIsErrUserNotFound, ..._GetGroupAPI_..., ..._APIKeyAccess_NotFoundAndNotOwnedSentinels, TestAuthServiceAdapter_GetUser_MissingUserIsErrUserNotFound, TestHandler_getUser_MissingUserIs404, TestHandler_getGroup_MissingGroupIs404, TestHandler_APIKeyDeleteRevoke_MissingOrNotOwnedIs404, and, each run alone, the 4 dashboard/inventory/coverage provider tests. I restored the PR's versions afterwards.
  • Commands. go build ./...; go vet with and without -tags integration on internal/{api,auth,server}; go test -race -short -count=1 on internal/{api,auth,server,mocks}, all ok; go test -race -tags integration -count=1 on internal/{auth,server,api}: all ok (auth 110s, server 62s, api 235s); golangci-lint v2.10.1 with and without the integration tag, 0 issues; gocyclo -over 10 on the changed non-test files, silent.

Body-stale: none found. The head is behind main, so the body's "re-checked against main a14dc8f" is older than main now, but I found nothing it says that is false.

Reviewer: rB27. 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 and others added 2 commits October 26, 2026 02:14
Brings the branch up to date with main (f316add).

Conflicted files:
- internal/api/handler_dashboard.go: main's LeanerCloud#1301 (issue LeanerCloud#72) changed the
  calculateCommitmentMetrics call in getDashboardSummary to return an
  error and fail the request on a purchase-history store error. This
  branch changed the same call to pass the provider value validated by
  providerFilterParam (so "all" means no filter) instead of the raw
  params["provider"]. Kept main's five-value call and its error return,
  with this branch's validated provider argument.

Everything else merged cleanly. Main's LeanerCloud#1271 loadUser refactor in
internal/auth leaves this branch's GetUser, GetGroupAPI and
authorizeAPIKeyAccess sentinels in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…account filters

Main's LeanerCloud#1100 added /api/accounts, /api/accounts/list,
/api/inventory/commitments and /api/inventory/coverage to openapi.yaml.
At the time of that change, their provider and enabled filters really
weren't validated, so it described them as "Not validated" or "Other
values are ignored". After the merge with this branch those
descriptions are wrong: the handlers now return 400 for an unknown
provider or enabled value, and accept "all" as "every provider".

This updates the four entries to match and adds the 400 response to
each one. It also adds "all" to the dashboard summary's provider enum,
since providerFilterParam accepts it there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Merged current main (f316add) into the branch and added one follow-up commit. New head: e02d6eb

Conflict resolution:

Follow-up commit e02d6eb (fix(api): ...) fixes a semantic conflict that merged without a textual conflict. Main's #1100 added /api/accounts, /api/accounts/list, /api/inventory/commitments and /api/inventory/coverage to openapi.yaml and described their filters as "Not validated" or "Other values are ignored". With this branch's validation, those descriptions are wrong. The commit updates them, adds the 400 response to each of the four entries, and adds all to the dashboard summary's provider enum.

Checks at e02d6eb:

  • go build, go vet (default, integration and e2e tags): clean.
  • golangci-lint v2.10.1 on internal/{api,auth,server}, with and without --build-tags integration: 0 issues. gocyclo -over 10 on the changed non-test files: silent.
  • go test -race -short -count=1 on internal/{api,auth,server,mocks}: ok.
  • go test -race -tags integration -count=1 on internal/{auth,server,api}: ok.
  • secrets scan: clean, and the planted key was flagged. scripts/test-git-secrets-allowlist.sh: 183 PASS, 0 FAIL.

@IAmJSD

IAmJSD commented Oct 26, 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 e02d6eb9ac4f90556991c4b89150422c927591bb

Blocking:

  • none

Non-blocking:

  • internal/api/openapi.yaml: the dashboard summary's provider now has enum: [aws, azure, gcp, all]. The four account/inventory provider params say "any other value is a 400" in prose but have no enum, so generated clients won't catch a bad value early. Worth adding the same enum there.
  • GET /api/auth/me (handler_auth.go around line 94) still turns a missing session user into a 500, and the other fmt.Errorf("API key not found") sites in service_apikeys_api.go are untouched. These are outside what the audit flagged and the PR body says so; noting them as follow-ups.
  • Partial fix: A02-007 is deferred to fix(api): four handler bugs found while documenting the API (#1102) #1141 and the A01-021 dashboard copy to the UI owner, so Refs #312 (not Closes) is correct.

Attacked:

  • Auth/scope (sensitive path):
    • In all four handlers the existing requirePermission/session check still runs before the new lookup or validation, so no 404 or 400 can be returned to a caller who lacks access.
    • authorizeAPIKeyAccess keeps the same owner/admin decision; only the error type changed. The not-owned and missing cases both map to the same 404 "API key not found", which removes an existence oracle instead of adding one.
    • Nothing matches on the strings "unauthorized" or "not found" in a way the new sentinels could change: I grepped internal/api for strings.Contains on error text. ErrAPIKeyNotOwned's message is unchanged.
    • The other GetUser callers (purchase notify, history email lookup, /auth/me) only log or propagate, and errors.Is(err, pgx.ErrNoRows) still holds because of the %w: %w wrapping.
  • Filters: validProviders includes "" and "all"; providerFilterParam maps "all" to "". fetchCommitmentPurchases, aggregateOnDemandByKey and the inventory in-memory filter all treat "" as no filter. The frontend only sends aws/azure/gcp or nothing, and enabled=true|false. I grepped the cli and mcp repos at origin/main and found no callers of these four endpoints.
  • Fail-before, reverting non-test code to the merge-base in a scratch checkout:
    • handler_users/groups/apikeys/dashboard/inventory: the new 404 tests and the dashboard/inventory provider tests fail (each provider test run on its own).
    • auth service_api.go/service_apikeys.go/service_user.go and server app.go: the 3 auth sentinel tests and the adapter test fail.
    • buildAccountFilter with the old logic kept under the new signature: TestListAccounts_RejectsUnknownEnumParams and TestListAccounts_AcceptedFilterValues fail.
    • All restored afterwards.
  • Ran on the head:
    • go build ./... and go vet with and without -tags integration on api/auth/server.
    • go test -race -short -count=1 on internal/api, auth and server: all pass.
    • go test -race -tags integration -count=1 on the same three packages: all pass.
    • golangci-lint v2.10.1, with and without --build-tags integration: 0 issues.
    • secrets-scan on the 19 changed files: clean, and the planted key was flagged.

Body-stale:

  • "openapi.yaml adds the 400 response to /api/dashboard/summary. The inventory and accounts list endpoints aren't in the spec." This is no longer true. Since e02d6eb, main's fix(api): document every router.go route in openapi.yaml and guard against drift #1100 entries for /api/accounts, /api/accounts/list, /api/inventory/commitments and /api/inventory/coverage are in the spec, and this PR adds the 400 response and the updated descriptions to each.
  • "around lines 425 and 474" for the remaining plain "API key not found" errors in service_apikeys_api.go: on the head they are at lines 406 and 455.
  • The combined-tree table lists fix/issue-312 at d2848a8. The head is e02d6eb (two main merges plus the openapi doc commit).

Reviewer: rB28. 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 26, 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 e02d6eb9ac4f90556991c4b89150422c927591bb

Blocking:

  • none

Non-blocking:

  • internal/auth/service_apikeys.go:262-272: the not-owned path still does loadUser + UserHasAdminCapability before answering, while the missing-key path returns right after GetAPIKeyByID. The response body and status are now identical (good), but the extra DB round trips make the two cases distinguishable by timing in principle. Low value since key IDs are random UUIDs and the caller already needs delete:api-keys; noting it only so nobody later relies on the 404 as a hard existence oracle.
  • internal/api/handler_auth.go:94 (/api/auth/me) and handler_history/purchases user lookups still pass the now-wrapped ErrUserNotFound through unmapped. That's unchanged behavior (500 / warn-and-skip as before), and out of the finding's scope; just flagging that a deleted user with a live session still gets a 500 on /me.
  • internal/api/handler_recommendations.go:74 still forwards the literal "all" to ListRecommendations, and the config store has no "all" special case, so GET /api/recommendations?provider=all still returns nothing, while this PR makes the dashboard/inventory/accounts reads treat all as no filter. That's the same bug class in a sibling handler the audit didn't list. Worth a follow-up so providerFilterParam is used everywhere.

Attacked:

  • Scope and auth: every touched handler still runs validateUUID/requirePermission before the new lookup or validation, so a 400 or 404 can't be reached unauthenticated. Not-owned and missing keys give byte-identical 404s. Checked every caller of Service.GetUser/GetGroupAPI for == pgx.ErrNoRows comparisons (none), and the wrapping keeps errors.Is(err, pgx.ErrNoRows) true. Checked the frontend senders (accounts.ts, plans.ts provider select, topbar-filters.ts, which already strips all): only aws|azure|gcp and true|false are sent, so the new 400s don't affect the UI. The cli and mcp repos don't call these endpoints.
  • Fail-before: I reverted the 9 production files (handler_apikeys/dashboard/groups/inventory/users, auth service_api/service_apikeys/service_user, server/app.go) to the merge-base. All the new api/auth/server tests then FAIL or panic on an unexpected mock call. I also put the old buildAccountFilter body back behind the new signature: TestListAccounts_RejectsUnknownEnumParams and TestListAccounts_AcceptedFilterValues both FAIL. Restored afterwards.
  • Ran at e02d6eb: go build ./...; go vet (plain and -tags integration) on api/auth/server; go test -race -short -count=1 on api/auth/server/mocks (all ok); golangci-lint v2.10.1 on api/auth/server, with and without --build-tags integration (0 issues); gocyclo -over 10 on the changed non-test files (silent); go test -race -tags integration -count=1 on auth, server and api: all three ok (auth 92s, server 28s, api 254s). git merge-tree against current origin/main (bee0627): merges clean.

Body-stale:

  • The combined-tree note about a fix/issue-72 vs fix/issue-312 conflict in handler_dashboard.go: the api: fetchCommitmentPurchases swallows store errors into zeroed dashboard KPIs #72 change ("purchase history is a hard dependency") is already on main and the branch has merged it, so there's nothing left to resolve.
  • "Other plain-string 'API key not found' errors ... around lines 425 and 474": at this head they're at service_apikeys_api.go:406 and :455.
  • "current main (a14dc8f)": main has moved on (the branch merged f316add; main is now bee0627). Cosmetic.

Reviewer: rB29. 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 26, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at e02d6eb9ac4f90556991c4b89150422c927591bb merged onto main (a4c07564), together with #1267 in that order (combined tree e44ea5c7, 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 26, 2026 03:06
@IAmJSD
IAmJSD merged commit b9b0daf into LeanerCloud:main Oct 26, 2026
40 checks passed
@IAmJSD

IAmJSD commented Oct 26, 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 e02d6eb9 (two required: money, auth or security path), green CI, and the combined-tree verification above. Squash-merged at exactly e02d6eb9ac4f90556991c4b89150422c927591bb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/few Limited audience priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/bug Defect urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant