diff --git a/internal/utils/user_utils.go b/internal/utils/user_utils.go index d94b3a20..636d61c1 100644 --- a/internal/utils/user_utils.go +++ b/internal/utils/user_utils.go @@ -11,6 +11,7 @@ import ( func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) { var users []model.LocalUser + seen := make(map[string]struct{}) if len(usersStr) == 0 { return nil, nil @@ -24,6 +25,10 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute if err != nil { return nil, err } + if _, dup := seen[parsed.Username]; dup { + return nil, fmt.Errorf("duplicate user %q, each username must be configured only once", parsed.Username) + } + seen[parsed.Username] = struct{}{} if attrs, ok := userAttributes[parsed.Username]; ok { parsed.Attributes = attrs } diff --git a/internal/utils/user_utils_test.go b/internal/utils/user_utils_test.go index 973be918..bde8bde8 100644 --- a/internal/utils/user_utils_test.go +++ b/internal/utils/user_utils_test.go @@ -10,6 +10,20 @@ import ( "github.com/tinyauthapp/tinyauth/internal/utils" ) +func TestGetUsersRejectsDuplicates(t *testing.T) { + hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G" + noAttrs := map[string]model.UserAttributes{} + + // The same username twice is rejected instead of silently keeping the first + _, err := utils.GetUsers([]string{"alice:" + hash, "alice:" + hash}, "", noAttrs) + assert.ErrorContains(t, err, `duplicate user "alice"`) + + // Distinct usernames still load + users, err := utils.GetUsers([]string{"alice:" + hash, "bob:" + hash}, "", noAttrs) + assert.NoError(t, err) + assert.Len(t, *users, 2) +} + func TestGetUsers(t *testing.T) { tmpDir := t.TempDir()