fleet/server/fleet/users_test.go
Jacob Shandling aadf82911a
Cap salt length before concatenating with plaintext for password updates (#17068)
## –> #16487
- Ensures length of salt is no larger than the key size. This ensures
the remaining space within the 72-byte limit for salt+hash is the
advertised 48 bytes, for the default salt size.
- Per [this
discussion](https://github.com/fleetdm/fleet/pull/16524#discussion_r1474675857),
we are assuming ASCII characters, and therefore 1byte:1char, for now –
unicode support may come later.
- Updated tests to follow.

## Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-02-28 11:16:02 -08:00

324 lines
7.5 KiB
Go

package fleet
import (
"fmt"
"testing"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
)
func TestIsGlobalObserver(t *testing.T) {
testCases := []struct {
GlobalRole *string
Expected bool
}{
{
GlobalRole: nil,
},
{
GlobalRole: ptr.String(RoleAdmin),
},
{
GlobalRole: ptr.String(RoleObserver),
Expected: true,
},
{
GlobalRole: ptr.String(RoleObserverPlus),
Expected: true,
},
}
for _, tC := range testCases {
sut := User{GlobalRole: tC.GlobalRole}
require.Equal(t, sut.IsGlobalObserver(), tC.Expected)
}
}
func TestTeamMembership(t *testing.T) {
teams := []UserTeam{
{
Role: RoleAdmin,
Team: Team{
ID: 1,
},
},
{
Role: RoleGitOps,
Team: Team{
ID: 2,
},
},
{
Role: RoleObserver,
Team: Team{
ID: 3,
},
},
{
Role: RoleObserver,
Team: Team{
ID: 4,
},
},
}
sut := User{}
require.Empty(t, sut.TeamMembership(func(ut UserTeam) bool {
return true
}))
sut.Teams = teams
var result []uint
pred := func(ut UserTeam) bool {
return ut.Role == RoleGitOps || ut.Role == RoleObserver
}
for k := range sut.TeamMembership(pred) {
result = append(result, k)
}
require.ElementsMatch(t, result, []uint{2, 3, 4})
result = make([]uint, 0, len(teams))
pred = func(ut UserTeam) bool {
return true
}
for k := range sut.TeamMembership(pred) {
result = append(result, k)
}
require.ElementsMatch(t, result, []uint{1, 2, 3, 4})
}
func TestValidatePassword(t *testing.T) {
passwordTests := []struct {
Password, Email string
Admin, PasswordReset bool
}{
{"foobar", "mike@fleet.co", true, false},
{"bar0baz!?", "jason@fleet.co", true, false},
}
for _, tt := range passwordTests {
user := newTestUser(t, tt.Password, tt.Email)
err := user.ValidatePassword(tt.Password)
assert.Nil(t, err)
err = user.ValidatePassword("different")
assert.NotNil(t, err)
}
}
func newTestUser(t *testing.T, password, email string) *User {
var (
salt = "test-salt"
cost = 10
)
withSalt := []byte(fmt.Sprintf("%s%s", password, salt))
hashed, _ := bcrypt.GenerateFromPassword(withSalt, cost)
return &User{
Salt: salt,
Password: hashed,
Email: email,
}
}
func TestUserPasswordRequirements(t *testing.T) {
passwordTests := []struct {
password string
wantErr bool
}{
{
password: "foobar",
wantErr: true,
},
{
password: "foobarbaz",
wantErr: true,
},
{
password: "foobarbaz!",
wantErr: true,
},
{
password: "foobarbaz!3",
wantErr: true,
},
{
password: "foobarbaz!3!",
},
}
for _, tt := range passwordTests {
t.Run(tt.password, func(t *testing.T) {
err := ValidatePasswordRequirements(tt.password)
if tt.wantErr {
assert.NotNil(t, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestSaltAndHashPassword(t *testing.T) {
goodTests := []string{"foobar!!", "bazbing!!", "foobarbaz!!!foobarbaz!!!foobarbaz!!!foobarbaz!!", "foobarbaz!!!foobarbaz!!!foobarbaz!!!foobarbaz!!!"}
keySize := 24
cost := 10
for _, pwd := range goodTests {
hashed, salt, err := saltAndHashPassword(keySize, pwd, cost)
require.NoError(t, err)
saltAndPass := []byte(fmt.Sprintf("%s%s", pwd, salt))
err = bcrypt.CompareHashAndPassword(hashed, saltAndPass)
require.NoError(t, err)
err = bcrypt.CompareHashAndPassword(hashed, []byte(fmt.Sprint("invalidpassword", salt)))
require.Error(t, err)
// too long
badTests := []string{"foobarbaz!!!foobarbaz!!!foobarbaz!!!foobarbaz!!!!"}
for _, pwd := range badTests {
_, _, err := saltAndHashPassword(keySize, pwd, cost)
require.Error(t, err)
}
}
}
func TestAdminCreateValidate(t *testing.T) {
testCases := []struct {
payload UserPayload
// if errContains is empty then no error is expected
errContains []string
}{
{
payload: UserPayload{},
errContains: []string{"name", "email", "password"},
},
{
payload: UserPayload{Name: ptr.String(""), Email: ptr.String(""), Password: ptr.String("")},
errContains: []string{"name", "email", "password"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String(""), Password: ptr.String("")},
errContains: []string{"email", "password"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("")},
errContains: []string{"password"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("foo")},
errContains: []string{"password"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("Foofoofoo1337#"), InviteToken: ptr.String("foo")},
errContains: []string{"invite_token"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("Foofoofoo1337#")},
errContains: nil,
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
err := tc.payload.VerifyAdminCreate()
if len(tc.errContains) == 0 {
require.NoError(t, err)
} else {
ierr := err.(*InvalidArgumentError)
require.Equal(t, len(tc.errContains), len(ierr.Errors))
for _, expected := range tc.errContains {
assertContainsErrorName(t, *ierr, expected)
}
}
})
}
}
func TestInviteCreateValidate(t *testing.T) {
testCases := []struct {
payload UserPayload
// if errContains is empty then no error is expected
errContains []string
}{
{
payload: UserPayload{},
errContains: []string{"name", "email", "password", "invite_token"},
},
{
payload: UserPayload{Name: ptr.String(""), Email: ptr.String(""), Password: ptr.String("")},
errContains: []string{"name", "email", "password", "invite_token"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String(""), Password: ptr.String("")},
errContains: []string{"email", "password", "invite_token"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("")},
errContains: []string{"password", "invite_token"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("foo")},
errContains: []string{"password", "invite_token"},
},
{
payload: UserPayload{Name: ptr.String("Foo"), Email: ptr.String("foo@example.com"), Password: ptr.String("Foofoofoo1337#"), InviteToken: ptr.String("foo")},
errContains: nil,
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
err := tc.payload.VerifyInviteCreate()
if len(tc.errContains) == 0 {
require.NoError(t, err)
} else {
ierr := err.(*InvalidArgumentError)
for _, expected := range tc.errContains {
require.Equal(t, len(tc.errContains), len(ierr.Errors))
assertContainsErrorName(t, *ierr, expected)
}
}
})
}
}
func TestValidateEmailError(t *testing.T) {
errCases := []string{
"invalid",
"Name Surname <test@example.com>",
"test.com",
"",
}
for _, c := range errCases {
require.Error(t, ValidateEmail(c))
}
}
func TestValidateEmail(t *testing.T) {
cases := []string{
"user@example.com",
"user@example.localhost",
"user+1@example.com",
}
for _, c := range cases {
require.NoError(t, ValidateEmail(c))
}
}
func assertContainsErrorName(t *testing.T, invalid InvalidArgumentError, name string) {
for _, argErr := range invalid.Errors {
if argErr.name == name {
return
}
}
t.Errorf("%v does not contain error %s", invalid, name)
}