Repository navigation
chore(auth): drop role-model leftovers, route user lookups through loadUser, correct post-fetch compare comments - #1271
Conversation
…adUser, correct post-fetch compare comments Dead code (A03-022): - delete the unused RoleAdmin/RoleUser/RoleReadOnly constants and name the seeded groups in the comments that referred to them; - delete Service.UpdateLastUsed and, with its only caller gone, the StoreInterface.UpdateAPIKeyLastUsed method, its Postgres implementation, mocks and tests; - delete credentials.ResolveGCPCredentials (no non-test caller); - drop SendWelcomeEmail from auth.EmailSenderInterface and its mocks (the auth service never sends one). Repeated helpers (A03-023): - loadUser now wraps store errors as "failed to get user: %w", and a new loadActiveUser adds the Active check; UpdateUserProfile, ChangePassword, GetUserPermissions, BuildAuthContext, CreateAPIKey, ListUserAPIKeys, authorizeAPIKeyAccess and the API-key validation path use them instead of inline copies (the API-key path said "user account not found" where every other path says "user not found"); - DeleteUser calls checkLastAdminConstraint instead of repeating it; - credentials: one roleSessionName(accountID) for AssumeRole and AssumeRoleWithWebIdentity. Post-fetch ConstantTimeCompare (A03-020): keep the four checks, which reject a row the store returned for a different hash, but replace the comments claiming they close a SQL timing oracle with what they do. Refs LeanerCloud#313 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 21 billable files and costs up to $5.25. 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: rB14. 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: rB15. 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 |
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>
Summary
Clears what can be cleared now from the four
internal/authfindings in #313 (audit findings A03-022, A03-023, A03-020 and A16-014). Each finding was re-checked against current main (a14dc8f2). The items left out are listed in the table with the reason. This is a partial fix: theUser.Saltcolumn drop, theredactEmailconsolidation and theMatchesAccountduplicate stay open, so this PR saysRefs, notCloses.Refs #313
Per-finding status
RoleAdmin/RoleUser/RoleReadOnlyconstantstypes.gothat named them now name the seeded groups (Administrators / Standard Users / Read-Only Users).Service.UpdateLastUsedStoreInterface.UpdateAPIKeyLastUsed, so that method, its Postgres implementation, its three pgxmock tests and its three mocks (auth.MockStore,mocks.MockAuthStore,serverhealth mock) are deleted too. The comment oneffectiveLifetimeUsagealready warned that any caller of it would break thelast_used_at/request_count_totalinvariant.credentials.ResolveGCPCredentialsinternal/can't be imported from outside the module.auth.EmailSenderInterface.SendWelcomeEmailMockEmailSender,recordingMailSink). The concreteemailsenders still implement it, so they still satisfy the narrower interface.User.Saltfieldusers.saltstill exists. The store INSERTs/UPDATEs/SELECTs it (store_postgres.go,store_postgres_credentials.go). Dropping it takes a migration plus a deploy order that stops writing the column before the drop, so it belongs in its own PR.loadUsercopiesloadUsernow wraps store errors asfailed to get user: %w. The newloadActiveUseradds theActivecheck. Callers routed through them:UpdateUserProfile,ChangePassword,CreateAPIKey(itsloadActiveUserForAPIKeycopy is gone),ListUserAPIKeys,authorizeAPIKeyAccess, the API-key validation path (lookupAPIKeyUseris gone), and alsoGetUserPermissionsandBuildAuthContext, which were byte-identical copies.DeleteUserrepeatscheckLastAdminConstraintisLastAdminConstraintViolation) is unchanged.sessionSuffixcomputed twicecredentials.roleSessionName(accountID)is shared by AssumeRole and AssumeRoleWithWebIdentity.redactEmailx3 +redactEmailLocalpkg/logging, is inLeanerCloud/cloud-commitments-go, whichgo.modpins, so this needs a release there first. The copies also mask differently: the auth one masks the domain too (us***@ex***.com), the other three don't. Choosing one rule changes what the logs show, which is an owner decision. Also, open PR #1152 (fix/issue-263) editsresetRecentlyIssued, including itsredactEmail(email)call line.MatchesAccountduplicatesAccountScope.Allowsfix/issue-1034). It deletesMatchesAccountandCanAccessAccountand rewritesAccountScope.Allows, which are the same lines.subtle.ConstantTimeComparecalls (ValidateSession,ValidateUserAPIKey,ResetTokenStatus,validateResetToken)permissionCoveredByreturn truenever reachedTestGrantCeiling_CarvedOutMayBeKeptNotWidened/kept_unchanged_is_allowed(added in 1f363bc, #1737) is the case the issue asks for: anadmin:*-only actor (admin:*does not reach the carved-out money verbs) resubmits a group's existingexecute:purchasesunchanged and the update must succeed. Mutation probe: changingreturn truetoreturn falseinpermissionCoveredBymakesgo test ./internal/auth/fail on exactly that test. The prerequisite fix, cli#1901, merged as 9ec4051 (#2069). I couldn't check the audit commit3c0f8ac9because it isn't in this repository's history.Why the constant-time compares stay (A03-020)
Each compare checks the stored hash against the hash the row was just selected by:
GetSession:WHERE token = $1 AND expires_at > NOW()GetAPIKeyByHash:WHERE key_hash = $1 AND is_active = trueGetUserByResetToken:WHERE password_reset_token = $1So with a correct store they always pass. The audit is right that they close no timing oracle: the database's equality has already run by then. That lookup timing is harmless anyway. The value compared is the SHA-256 of a 32-byte
crypto/randtoken or key (generateToken,CreateAPIKey), and an attacker can neither guess it nor steer it byte by byte.The compares are still a store-integrity check: they reject a row the store returned for a different credential. The brief for this change was not to remove a compare that sits on a credential path unless it is provably bypassed, and these aren't bypassed, only redundant with a correct store. So the four checks stay and only the false comments are replaced. The tests that cover them were renamed from "constant-time mismatch …" to "stored hash mismatch …" and their comments describe the store-integrity check. No behavior change.
Not done here: the issue also asks to correct #104's "verified sound" section, which lists these compares as a timing defense. That's an issue comment, so it's left to the maintainer.
Behavior changes (all error text, no control flow)
UpdateUser,DeleteUser,ChangePassword,GetUserPermissions,BuildAuthContextandValidateSessionare now wrapped asfailed to get user: <err>(with%w, soerrors.Isstill works). The API-key paths andUpdateUserProfilealready did this. ForValidateSessionthe result readsfailed to load session user: failed to get user: <err>.user not foundinstead ofuser account not found, matching every other path. Nothing in the repo matched the old string.Verification
All unit or mock evidence, on this branch. Every command ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true.go build ./...,go vet ./...andgo vet -tags integration ./...: clean.go test -count=1run one package at a time on./internal/auth/,./internal/credentials/,./internal/mocks/,./internal/server/and./internal/api/: allok.golangci-lint run --allow-parallel-runners(v2.10.1) on./internal/auth/... ./internal/credentials/... ./internal/mocks/... ./internal/server/..., plus--build-tags integration ./internal/auth/...:0 issues.gocyclo -over 10 -ignore "_test\.go"on the changed non-test files: no output.git merge-treeof this commit against fix(auth): bind group account scope entries to account IDs #1116, test(auth): cover remaining auth failure paths; fix invite re-send and sticky KMS signer errors #1152,fork/fix/issue-64,fork/fix/issue-169andfork/fix/issue-310: all merge cleanly. None of those diffs adds a use of a symbol deleted here. test(auth): cover remaining auth failure paths; fix invite re-send and sticky KMS signer errors #1152's newinviteMailSinkkeeps aSendWelcomeEmailmethod, which is harmless against the narrower interface.Fail-before / pass-after for the new
internal/auth/service_user_lookup_test.go. It pins one lookup contract on all ten routed callers:pgx.ErrNoRows→user not found, a nil user →user not found, a store error →failed to get user: …witherrors.Is, and an inactive owner →user account is not activeon the two active-only paths. I copied the test file unchanged onto a cleanorigin/mainworktree:On this branch:
ok.internal/credentials/resolver_test.go: TestRoleSessionName_SharedByAssumeRoleAndWebIdentityrecords theRoleSessionNamethat both the AssumeRole and the web-identity provider send to a fake STS client, for 22-, 8- and 5-character IDs. It is a pin for the refactor, not a fail-before test: both paths already sent the same value.A16-014 mutation probe, on main:
sed -i '444s/return true/return false/' internal/auth/group_ceiling.gogives--- FAIL: TestGrantCeiling_CarvedOutMayBeKeptNotWidened. The file was restored afterwards.No DB schema or query changed: the only store change deletes an unused method. So there's no new integration test and no concurrency surface.
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 / follow-ups
User.Salt/users.saltcolumn drop (see table).redactEmailconsolidation: needscloud-commitments-gopkg/loggingand an owner decision on the masking rule (see table).MatchesAccountvsAccountScope.Allows: handled by fix(auth): bind group account scope entries to account IDs #1116.email.SenderInterface.SendWelcomeEmailand its template and implementations ininternal/email(plus the mocks ininternal/mocks,api,server,scheduler,purchase) have no production caller anywhere either. Deleting them is a separateinternal/emailcleanup.MFASetup,MFAEnable,MFADisable,MFARegenerateRecoveryCodes) still callGetUserByIDdirectly. That's deliberate: they map every lookup failure toErrMFAAuthFailed, so they were left alone.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