Skip to content

Fix/password policy hash 330#331

Open
minhpham1810 wants to merge 2 commits intocontainer-registry:mainfrom
minhpham1810:fix/password-policy-hash-330
Open

Fix/password policy hash 330#331
minhpham1810 wants to merge 2 commits intocontainer-registry:mainfrom
minhpham1810:fix/password-policy-hash-330

Conversation

@minhpham1810
Copy link

@minhpham1810 minhpham1810 commented Feb 15, 2026

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.PasswordPolicyError so 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.

  • Bug Fixes
    • Validate passwords in HashPassword using the env-configured policy; return 400 for policy violations via PasswordPolicyError and 500 for hashing failures.
    • Removed duplicate validation in server handlers and bootstrap; updated tests to reflect centralized enforcement.

Written for commit b0b9c81. Summary will update on new commits.

Summary by CodeRabbit

  • Documentation

    • Added Go Report Card badge to README.
  • Bug Fixes

    • Improved password validation and error messaging during user creation and password changes.
    • Streamlined password handling to remove redundant hashing and return clearer client-facing errors for invalid passwords.
  • Tests

    • Updated password-related tests and test cases to align with validation changes.

Signed-off-by: Minh Pham <kp025@bucknell.edu>
@github-actions github-actions bot added documentation Improvements or additions to documentation golang labels Feb 15, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

Centralizes password policy validation inside HashPassword by adding a PasswordPolicyError type and removing duplicate validation checks from bootstrap and user handlers; README updated with a Go Report Card badge. (46 words)

Changes

Cohort / File(s) Summary
Auth package
ground-control/internal/auth/password.go, ground-control/internal/auth/password_test.go
Adds PasswordPolicyError (with Error() and Unwrap()), loads policy in HashPassword and returns PasswordPolicyError on validation failure; tests updated to expect error cases and include additional password samples.
Server bootstrap
ground-control/internal/server/bootstrap.go
Removes direct passwordPolicy.Validate() call; handles PasswordPolicyError returned from HashPassword via errors.As to map to a 400-style message.
User handlers
ground-control/internal/server/user_handlers.go
Removes inline password policy checks in createUserHandler, changeOwnPasswordHandler, and changeUserPasswordHandler; delegates validation to HashPassword and distinguishes PasswordPolicyError (400) from other errors (500).
Docs
README.md
Adds a Go Report Card badge link at the top of the README.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Suggested reviewers

  • Vad1mo
  • amands98
🚥 Pre-merge checks | ✅ 3 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The README.md change (Go Report Card badge) is out of scope relative to the linked issue #330, which focuses solely on password policy enforcement in the auth module. Remove the README.md change unrelated to password policy enforcement, or create a separate PR for documentation updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/password policy hash 330' is vague and references only the issue number without clearly describing the main change. Improve the title to clearly describe the primary change, such as 'Centralize password policy enforcement in HashPassword' or 'Enforce password policy directly in auth.HashPassword'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description comprehensively covers the issue, implementation approach, rationale, and testing steps, matching the template structure with issue reference, description, and context.
Linked Issues check ✅ Passed The PR successfully implements all requirements from issue #330: centralizes password policy validation in HashPassword(), introduces PasswordPolicyError for consistent error handling, removes duplicate validation logic across handlers, and updates tests accordingly.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

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

@codacy-production
Copy link

codacy-production bot commented Feb 15, 2026

Codacy's Analysis Summary

0 new issue (≤ 0 issue)
0 new security issue
0 complexity
0 duplications

Review Pull Request in Codacy →

AI Reviewer available: add the codacy-review label to get contextual insights without leaving GitHub.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 HashPassword implicitly 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().MaxLength is 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().MaxLength as a separate wantErr: true case, 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 on errors.As(err, &pe) to distinguish policy violations from hashing failures. The tests currently only check require.Error(t, err) without verifying the error type, so a regression that returns a plain error instead of PasswordPolicyError would 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 before VerifyPassword(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 repeated PasswordPolicyError handling pattern.

The same errors.As → 400 / fallback → 500 block is repeated in createUserHandler, changeOwnPasswordHandler, changeUserPasswordHandler, and bootstrap.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>
@minhpham1810 minhpham1810 force-pushed the fix/password-policy-hash-330 branch from 5403956 to b0b9c81 Compare February 15, 2026 23:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Verify 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 HashPassword call. Since the policy doesn't change at runtime, consider loading it once (e.g., at startup or via sync.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 wantErr is true, the test only checks that an error occurred. Asserting that the error is a *PasswordPolicyError via errors.As would 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 the auth import if the test is in the same package — since it's package 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 that DefaultPolicy() 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 using t.Setenv() to make expectations deterministic.

ground-control/internal/server/user_handlers.go (1)

62-66: Optional: Extract duplicated PasswordPolicyError handling into a helper.

The same errors.As → 400 / fallback → 500 pattern is repeated in three handlers. A small helper like writeHashError(w, err) would reduce duplication and make future changes (e.g., logging) easier to apply consistently.

Also applies to: 208-212, 264-268

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

Labels

documentation Improvements or additions to documentation golang

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Password policy is not enforced before hashing in auth module

1 participant