mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
Add fast test hasher to speed up CI tests (#34707)
The production password hasher uses PBKDF2 with 600,000 iterations, which is slow especially when combined with race detection. This adds a fast test hasher (work factor 1) that can be used during tests to speed up user creation. The fast hasher is only available in non-production builds via build tags, ensuring it cannot be used in production.
This commit is contained in:
parent
c519789529
commit
219530c82c
9 changed files with 190 additions and 6 deletions
|
|
@ -469,7 +469,7 @@ func TestCheckUserPassword(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("successful migration from PBKDF2 with old parameter to new parameter", func(t *testing.T) {
|
||||
// Create a PBKDF2 hasher with work factor = 10000 instead of the default 60000
|
||||
// Create a PBKDF2 hasher with work factor = 10000 instead of the default
|
||||
oldParamPBKDF2, err := hashers.NewPBKDF2(10000, 32)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -488,8 +488,8 @@ func TestCheckUserPassword(t *testing.T) {
|
|||
require.Nil(t, appErr)
|
||||
require.NotEqual(t, pwdBcrypt, updatedUser.Password)
|
||||
require.Contains(t, updatedUser.Password, "$pbkdf2")
|
||||
// The new user hash contains the new parameter
|
||||
require.Contains(t, updatedUser.Password, "w=60000")
|
||||
// The new user hash should NOT contain the old parameter
|
||||
require.NotContains(t, updatedUser.Password, "w=10000")
|
||||
|
||||
// Re-check with updated password
|
||||
appErr = th.App.checkUserPassword(user, pwd, false)
|
||||
|
|
|
|||
|
|
@ -135,17 +135,17 @@ func GetHasherFromPHCString(phcString string) (PasswordHasher, phcparser.PHC, er
|
|||
|
||||
// Hash hashes the provided password with the latest hashing method.
|
||||
func Hash(password string) (string, error) {
|
||||
return latestHasher.Hash(password)
|
||||
return getLatestHasher().Hash(password)
|
||||
}
|
||||
|
||||
// CompareHashAndPassword compares the parsed [phcparser.PHC] and the provided
|
||||
// password using the latest hashing method.
|
||||
func CompareHashAndPassword(phc phcparser.PHC, password string) error {
|
||||
return latestHasher.CompareHashAndPassword(phc, password)
|
||||
return getLatestHasher().CompareHashAndPassword(phc, password)
|
||||
}
|
||||
|
||||
// IsLatestHasher verifies that the provided hasher is the latest one. This
|
||||
// function is useful for identifying stored hashes that require a migration.
|
||||
func IsLatestHasher(hasher PasswordHasher) bool {
|
||||
return latestHasher == hasher
|
||||
return getLatestHasher() == hasher
|
||||
}
|
||||
|
|
|
|||
56
server/channels/app/password/hashers/hashers_dev.go
Normal file
56
server/channels/app/password/hashers/hashers_dev.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build !production
|
||||
|
||||
package hashers
|
||||
|
||||
import "testing"
|
||||
|
||||
// testHasher is used during tests to override the latestHasher with a faster
|
||||
// alternative. This should only be set via SetTestHasher and only in test code.
|
||||
var testHasher PasswordHasher
|
||||
|
||||
// getLatestHasher returns the hasher to use for password operations.
|
||||
// In non-production builds, if a test hasher has been set via SetTestHasher,
|
||||
// it will be returned instead of the production latestHasher.
|
||||
func getLatestHasher() PasswordHasher {
|
||||
if testHasher != nil {
|
||||
return testHasher
|
||||
}
|
||||
return latestHasher
|
||||
}
|
||||
|
||||
// SetTestHasher sets a hasher to be used instead of the latestHasher during tests.
|
||||
// This is useful for speeding up tests that create many users, as password hashing
|
||||
// is computationally expensive. Pass nil to restore normal behavior.
|
||||
//
|
||||
// This function is only available in non-production builds and should only be
|
||||
// called from test code, typically in TestMain. It will panic if called outside
|
||||
// of a test context.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// func TestMain(m *testing.M) {
|
||||
// hashers.SetTestHasher(hashers.FastTestHasher())
|
||||
// os.Exit(m.Run())
|
||||
// }
|
||||
func SetTestHasher(h PasswordHasher) {
|
||||
if !testing.Testing() {
|
||||
panic("SetTestHasher called outside of test context")
|
||||
}
|
||||
testHasher = h
|
||||
}
|
||||
|
||||
// FastTestHasher returns a PBKDF2 hasher configured with minimal work factor
|
||||
// for use in tests while still producing valid password hashes that can be
|
||||
// verified.
|
||||
//
|
||||
// This function is only available in non-production builds.
|
||||
func FastTestHasher() PasswordHasher {
|
||||
h, err := NewPBKDF2(1, defaultKeyLength)
|
||||
if err != nil {
|
||||
panic("failed to create fast test hasher: " + err.Error())
|
||||
}
|
||||
return h
|
||||
}
|
||||
86
server/channels/app/password/hashers/hashers_dev_test.go
Normal file
86
server/channels/app/password/hashers/hashers_dev_test.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build !production
|
||||
|
||||
package hashers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetTestHasher(t *testing.T) {
|
||||
// Ensure testHasher starts as nil
|
||||
SetTestHasher(nil)
|
||||
|
||||
// Hash should work with nil testHasher (uses latestHasher)
|
||||
hash1, err := Hash("password")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, hash1)
|
||||
|
||||
// Set a fast test hasher
|
||||
fastHasher := FastTestHasher()
|
||||
SetTestHasher(fastHasher)
|
||||
defer SetTestHasher(nil)
|
||||
|
||||
// Hash should now use the fast test hasher
|
||||
hash2, err := Hash("password")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, hash2)
|
||||
|
||||
// Verify the hash was generated with different parameters
|
||||
// The fast hasher uses work factor 1, so the hash should contain w=1
|
||||
require.Contains(t, hash2, "w=1")
|
||||
|
||||
// Verify the password can be verified against the hash
|
||||
hasher, phc, err := GetHasherFromPHCString(hash2)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, hasher.CompareHashAndPassword(phc, "password"))
|
||||
require.Error(t, hasher.CompareHashAndPassword(phc, "wrongpassword"))
|
||||
}
|
||||
|
||||
func TestFastTestHasher(t *testing.T) {
|
||||
hasher := FastTestHasher()
|
||||
require.NotNil(t, hasher)
|
||||
|
||||
// Verify it's a PBKDF2 hasher with work factor 1
|
||||
pbkdf2Hasher, ok := hasher.(PBKDF2)
|
||||
require.True(t, ok, "FastTestHasher should return a PBKDF2 hasher")
|
||||
require.Equal(t, 1, pbkdf2Hasher.workFactor)
|
||||
|
||||
// Test that it produces valid hashes
|
||||
hash, err := hasher.Hash("testpassword")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, hash, "w=1")
|
||||
|
||||
// Verify the hash can be validated
|
||||
parsedHasher, phc, err := GetHasherFromPHCString(hash)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, parsedHasher.CompareHashAndPassword(phc, "testpassword"))
|
||||
}
|
||||
|
||||
func TestGetLatestHasher(t *testing.T) {
|
||||
// Ensure testHasher starts as nil
|
||||
SetTestHasher(nil)
|
||||
|
||||
// Without test hasher, should return latestHasher
|
||||
require.Equal(t, latestHasher, getLatestHasher())
|
||||
|
||||
// Set a fast test hasher
|
||||
fastHasher := FastTestHasher()
|
||||
SetTestHasher(fastHasher)
|
||||
defer SetTestHasher(nil)
|
||||
|
||||
// With test hasher set, should return the test hasher
|
||||
require.Equal(t, fastHasher, getLatestHasher())
|
||||
require.NotEqual(t, latestHasher, getLatestHasher())
|
||||
}
|
||||
|
||||
func BenchmarkFastTestHasher(b *testing.B) {
|
||||
hasher := FastTestHasher()
|
||||
for b.Loop() {
|
||||
_, _ = hasher.Hash("password")
|
||||
}
|
||||
}
|
||||
12
server/channels/app/password/hashers/hashers_production.go
Normal file
12
server/channels/app/password/hashers/hashers_production.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build production
|
||||
|
||||
package hashers
|
||||
|
||||
// getLatestHasher returns the hasher to use for password operations.
|
||||
// In production builds, this always returns the latestHasher.
|
||||
func getLatestHasher() PasswordHasher {
|
||||
return latestHasher
|
||||
}
|
||||
|
|
@ -136,3 +136,10 @@ func TestIsLatestHasher(t *testing.T) {
|
|||
require.Equal(t, tc.expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDefaultHasher(b *testing.B) {
|
||||
hasher := DefaultPBKDF2()
|
||||
for b.Loop() {
|
||||
_, _ = hasher.Hash("password")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
server/channels/testlib/hashers_dev.go
Normal file
12
server/channels/testlib/hashers_dev.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build !production
|
||||
|
||||
package testlib
|
||||
|
||||
import "github.com/mattermost/mattermost/server/v8/channels/app/password/hashers"
|
||||
|
||||
func setupFastTestHasher() {
|
||||
hashers.SetTestHasher(hashers.FastTestHasher())
|
||||
}
|
||||
8
server/channels/testlib/hashers_production.go
Normal file
8
server/channels/testlib/hashers_production.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
//go:build production
|
||||
|
||||
package testlib
|
||||
|
||||
func setupFastTestHasher() {}
|
||||
|
|
@ -87,6 +87,9 @@ func NewMainHelperWithOptions(options *HelperOptions) *MainHelper {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Use a fast password hasher during tests to speed up user creation.
|
||||
setupFastTestHasher()
|
||||
|
||||
if options != nil {
|
||||
mainHelper.Options = *options
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue