Fix/password policy hash 330#331
Fix/password policy hash 330#331minhpham1810 wants to merge 2 commits intocontainer-registry:mainfrom
Conversation
Signed-off-by: Minh Pham <kp025@bucknell.edu>
📝 WalkthroughWalkthroughCentralizes password policy validation inside Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codacy's Analysis Summary0 new issue (≤ 0 issue) Review Pull Request in Codacy →
|
There was a problem hiding this comment.
🧹 Nitpick comments (5)
ground-control/internal/auth/password.go (1)
20-25:LoadPolicyFromEnv()is called on every hash invocation.This re-reads ~6 environment variables per call. Given that Argon2 hashing dominates the cost, the overhead is negligible. However, if this concern grows (e.g., policy becomes more complex), consider accepting the policy as a parameter or caching it.
One design trade-off worth noting: this makes
HashPasswordimplicitly dependent on the process environment, which can make unit testing less deterministic unless env vars are explicitly set/unset in each test. Currently the tests rely on default policy, which works — just be aware if the defaults ever change, tests may break silently.ground-control/internal/auth/password_test.go (2)
77-83: Fragile test: pass/fail depends on an implicit default policy value.The comment on Line 82 ("flip to true if you change this to exceed policy MaxLength") shifts the burden to future maintainers. If
DefaultPolicy().MaxLengthis ever lowered below the length of this string (~90 chars), the test breaks with a non-obvious error.Consider either:
- Explicitly generating a password that exceeds
DefaultPolicy().MaxLengthas a separatewantErr: truecase, or- Asserting the length against the loaded policy in the test itself.
94-97: Consider asserting the error type is*PasswordPolicyError.Callers (
bootstrap.go,user_handlers.go) rely onerrors.As(err, &pe)to distinguish policy violations from hashing failures. The tests currently only checkrequire.Error(t, err)without verifying the error type, so a regression that returns a plain error instead ofPasswordPolicyErrorwould go undetected.💡 Suggested addition
if tt.wantErr { require.Error(t, err) + var pe *PasswordPolicyError + require.True(t, errors.As(err, &pe), "expected PasswordPolicyError, got %T", err) return }(Requires adding
"errors"to the import block.)ground-control/internal/server/user_handlers.go (2)
206-230: New password is hashed before verifying the current password.
HashPassword(req.NewPassword)(Line 206) runs the expensive Argon2 computation beforeVerifyPassword(req.CurrentPassword, ...)(Line 226). If the current password is wrong, the hash work is wasted. More importantly from a UX perspective, the user could receive a "new password doesn't meet policy" error when their real problem is an incorrect current password.Consider swapping the order: verify the current password first, then hash the new one.
♻️ Suggested reorder
- hash, err := auth.HashPassword(req.NewPassword) - if err != nil { - var pe *auth.PasswordPolicyError - if errors.As(err, &pe) { - WriteJSONError(w, pe.Error(), http.StatusBadRequest) - return - } - - WriteJSONError(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Verify current password user, err := s.dbQueries.GetUserByUsername(r.Context(), currentUser.Username) if err != nil { WriteJSONError(w, "Internal server error", http.StatusInternalServerError) return } valid := auth.VerifyPassword(req.CurrentPassword, user.PasswordHash) if !valid { WriteJSONError(w, "Current password is incorrect", http.StatusUnauthorized) return } + hash, err := auth.HashPassword(req.NewPassword) + if err != nil { + var pe *auth.PasswordPolicyError + if errors.As(err, &pe) { + WriteJSONError(w, pe.Error(), http.StatusBadRequest) + return + } + WriteJSONError(w, "Internal server error", http.StatusInternalServerError) + return + } if err := s.dbQueries.UpdateUserPassword(r.Context(), database.UpdateUserPasswordParams{
60-70: Consider extracting a helper for the repeatedPasswordPolicyErrorhandling pattern.The same
errors.As→ 400 / fallback → 500 block is repeated increateUserHandler,changeOwnPasswordHandler,changeUserPasswordHandler, andbootstrap.go. A small helper would reduce duplication:💡 Example helper
// handleHashError writes the appropriate error response for a HashPassword failure. // Returns true if the error was handled (response written). func handleHashError(w http.ResponseWriter, err error) { var pe *auth.PasswordPolicyError if errors.As(err, &pe) { WriteJSONError(w, pe.Error(), http.StatusBadRequest) return } WriteJSONError(w, "Internal server error", http.StatusInternalServerError) }Then each handler simplifies to:
hash, err := auth.HashPassword(req.Password) if err != nil { handleHashError(w, err) return }
Signed-off-by: Minh Pham <kp025@bucknell.edu>
5403956 to
b0b9c81
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ground-control/internal/server/user_handlers.go (1)
206-230:⚠️ Potential issue | 🟠 MajorVerify current password before hashing the new one.
The new password is hashed (an expensive Argon2 operation) at Line 206 before the current password is verified at Line 226. If the current password is wrong, the hashing work is wasted. More importantly, the logical flow should authenticate the user's identity before processing the password change.
Swap the order: verify the current password first, then hash the new password.
Suggested reorder
- hash, err := auth.HashPassword(req.NewPassword) - if err != nil { - var pe *auth.PasswordPolicyError - if errors.As(err, &pe) { - WriteJSONError(w, pe.Error(), http.StatusBadRequest) - return - } - - WriteJSONError(w, "Internal server error", http.StatusInternalServerError) - return - } - - // Verify current password user, err := s.dbQueries.GetUserByUsername(r.Context(), currentUser.Username) if err != nil { WriteJSONError(w, "Internal server error", http.StatusInternalServerError) return } valid := auth.VerifyPassword(req.CurrentPassword, user.PasswordHash) if !valid { WriteJSONError(w, "Current password is incorrect", http.StatusUnauthorized) return } + hash, err := auth.HashPassword(req.NewPassword) + if err != nil { + var pe *auth.PasswordPolicyError + if errors.As(err, &pe) { + WriteJSONError(w, pe.Error(), http.StatusBadRequest) + return + } + + WriteJSONError(w, "Internal server error", http.StatusInternalServerError) + return + } if err := s.dbQueries.UpdateUserPassword(r.Context(), database.UpdateUserPasswordParams{
🧹 Nitpick comments (4)
ground-control/internal/auth/password.go (1)
20-25:LoadPolicyFromEnv()is called on every hash invocation.This re-reads ~6 environment variables and parses them on every
HashPasswordcall. Since the policy doesn't change at runtime, consider loading it once (e.g., at startup or viasync.Once) and passing it in or caching it. Not a correctness issue, but unnecessary repeated I/O on a hot path.ground-control/internal/auth/password_test.go (2)
94-97: Consider asserting the error type, not just its existence.When
wantErris true, the test only checks that an error occurred. Asserting that the error is a*PasswordPolicyErrorviaerrors.Aswould strengthen the test and verify the contract introduced by this PR.💡 Suggested improvement
if tt.wantErr { require.Error(t, err) + var pe *auth.PasswordPolicyError + require.True(t, errors.As(err, &pe), "expected PasswordPolicyError, got %T", err) return }(Would also need to add
"errors"and theauthimport if the test is in the same package — since it'spackage auth, just"errors"is needed.)
77-82: Fragile test: behavior depends on unset environment variables.This test (and others) implicitly depend on
PASSWORD_*env vars not being set so thatDefaultPolicy()is used. If CI or a developer's shell exports any of these vars, test results change silently. Consider explicitly setting the relevant env vars in tests usingt.Setenv()to make expectations deterministic.ground-control/internal/server/user_handlers.go (1)
62-66: Optional: Extract duplicatedPasswordPolicyErrorhandling into a helper.The same
errors.As→ 400 / fallback → 500 pattern is repeated in three handlers. A small helper likewriteHashError(w, err)would reduce duplication and make future changes (e.g., logging) easier to apply consistently.Also applies to: 208-212, 264-268
Description
This PR implements Issue #330 by enforcing the configured password policy directly inside
auth.HashPassword().Additional context
Changes
Added password policy validation inside auth.HashPassword()
Introduced
auth.PasswordPolicyErrorso callers can treat policy violations as client errors (400) while keeping unexpected hashing failures as server errors (500)Updated password hashing tests to reflect the new behavior (invalid passwords can no longer be hashed)
Why
Previously, password validation was performed inconsistently at call sites, which risked missing validation in new code paths. Centralizing validation in
HashPassword()ensures all password hashes in the system enforce the same policy.Testing
go test ./... in ground-control/Fixes #330.
Summary by cubic
Centralized password policy enforcement in auth.HashPassword with PasswordPolicyError so clients get 400 on policy violations and 500 on unexpected hash errors. Handlers and bootstrap now use this; tests updated; added a Go Report Card badge to README; fixes #330.
Written for commit b0b9c81. Summary will update on new commits.
Summary by CodeRabbit
Documentation
Bug Fixes
Tests