Skip to content

Commit a941bfe

Browse files
authored
Fix group name validation (#2402)
- Move lowercase validation to primary validator - Remove validation from manager since it's already used in CLI and API Fix #2355
1 parent 9a9e934 commit a941bfe

File tree

4 files changed

+29
-37
lines changed

4 files changed

+29
-37
lines changed

pkg/groups/group_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ func TestManager_Create(t *testing.T) {
8787
groupName: "MyGroup",
8888
setupMock: func(_ *mocks.MockStore) {}, // validation fails before store access
8989
expectError: true,
90-
errorMsg: "invalid group name",
90+
errorMsg: "must be lowercase",
9191
},
9292
{
9393
name: "invalid name - mixed case",
9494
groupName: "DefAult",
9595
setupMock: func(_ *mocks.MockStore) {}, // validation fails before store access
9696
expectError: true,
97-
errorMsg: "invalid group name",
97+
errorMsg: "must be lowercase",
9898
},
9999
}
100100

pkg/groups/manager.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,14 @@ import (
1010
thverrors "github.com/stacklok/toolhive/pkg/errors"
1111
"github.com/stacklok/toolhive/pkg/logger"
1212
"github.com/stacklok/toolhive/pkg/state"
13+
"github.com/stacklok/toolhive/pkg/validation"
1314
)
1415

1516
const (
1617
// DefaultGroupName is the name of the default group
1718
DefaultGroupName = "default"
1819
)
1920

20-
// ValidateGroupName enforces lowercase-only group names.
21-
func ValidateGroupName(name string) error {
22-
if name != strings.ToLower(name) {
23-
return fmt.Errorf("invalid group name: %q (must be lowercase)", name)
24-
}
25-
return nil
26-
}
27-
2821
// manager implements the Manager interface
2922
type manager struct {
3023
groupStore state.Store
@@ -42,9 +35,9 @@ func NewManager() (Manager, error) {
4235

4336
// Create creates a new group with the given name
4437
func (m *manager) Create(ctx context.Context, name string) error {
45-
// Enforce lowercase group names
46-
if err := ValidateGroupName(name); err != nil {
47-
return err
38+
// Validate group name
39+
if err := validation.ValidateGroupName(name); err != nil {
40+
return thverrors.NewInvalidArgumentError(err.Error(), err)
4841
}
4942
// Check if group already exists
5043
exists, err := m.groupStore.Exists(ctx, name)
@@ -64,9 +57,6 @@ func (m *manager) Create(ctx context.Context, name string) error {
6457

6558
// Get retrieves a group by name
6659
func (m *manager) Get(ctx context.Context, name string) (*Group, error) {
67-
if err := ValidateGroupName(name); err != nil {
68-
return nil, err
69-
}
7060
reader, err := m.groupStore.GetReader(ctx, name)
7161
if err != nil {
7262
return nil, fmt.Errorf("failed to get reader for group: %w", err)
@@ -107,17 +97,11 @@ func (m *manager) List(ctx context.Context) ([]*Group, error) {
10797

10898
// Delete removes a group by name
10999
func (m *manager) Delete(ctx context.Context, name string) error {
110-
if err := ValidateGroupName(name); err != nil {
111-
return err
112-
}
113100
return m.groupStore.Delete(ctx, name)
114101
}
115102

116103
// Exists checks if a group exists
117104
func (m *manager) Exists(ctx context.Context, name string) (bool, error) {
118-
if err := ValidateGroupName(name); err != nil {
119-
return false, err
120-
}
121105
return m.groupStore.Exists(ctx, name)
122106
}
123107

pkg/validation/validation.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"golang.org/x/net/http/httpguts"
1010
)
1111

12-
var validGroupNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-\s]+$`)
12+
var validGroupNameRegex = regexp.MustCompile(`^[a-z0-9_\-\s]+$`)
1313

1414
// ValidateGroupName validates that a group name only contains allowed characters:
15-
// alphanumeric, underscore, dash, and space.
15+
// lowercase alphanumeric, underscore, dash, and space.
1616
// It also enforces no leading/trailing/consecutive spaces and disallows null bytes.
1717
func ValidateGroupName(name string) error {
1818
if name == "" || strings.TrimSpace(name) == "" {
@@ -24,9 +24,14 @@ func ValidateGroupName(name string) error {
2424
return fmt.Errorf("group name cannot contain null bytes")
2525
}
2626

27+
// Enforce lowercase-only group names
28+
if name != strings.ToLower(name) {
29+
return fmt.Errorf("group name must be lowercase")
30+
}
31+
2732
// Validate characters
2833
if !validGroupNameRegex.MatchString(name) {
29-
return fmt.Errorf("group name can only contain alphanumeric characters, underscores, dashes, and spaces: %q", name)
34+
return fmt.Errorf("group name can only contain lowercase alphanumeric characters, underscores, dashes, and spaces: %q", name)
3035
}
3136

3237
// Check for leading/trailing whitespace

pkg/validation/validation_test.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,36 @@ func TestValidateGroupName(t *testing.T) {
1717
expectErr bool
1818
}{
1919
// ✅ Valid cases
20-
{"valid_simple_name", "TeamAlpha", false},
21-
{"valid_with_spaces", "Team Alpha", false},
22-
{"valid_with_dash_and_underscore", "Team-Alpha_123", false},
20+
{"valid_simple_name", "teamalpha", false},
21+
{"valid_with_spaces", "team alpha", false},
22+
{"valid_with_dash_and_underscore", "team-alpha_123", false},
2323

2424
// ❌ Empty or whitespace-only
2525
{"empty_string", "", true},
2626
{"only_spaces", " ", true},
2727

2828
// ❌ Invalid characters
29-
{"invalid_special_characters", "Team@Alpha!", true},
29+
{"invalid_special_characters", "team@alpha!", true},
3030
{"invalid_unicode", "团队🚀", true},
3131

3232
// ❌ Null byte
33-
{"null_byte", "Team\x00Alpha", true},
33+
{"null_byte", "team\x00alpha", true},
3434

3535
// ❌ Leading/trailing whitespace
36-
{"leading_space", " TeamAlpha", true},
37-
{"trailing_space", "TeamAlpha ", true},
36+
{"leading_space", " teamalpha", true},
37+
{"trailing_space", "teamalpha ", true},
3838

3939
// ❌ Consecutive spaces
40-
{"consecutive_spaces_middle", "Team Alpha", true},
41-
{"consecutive_spaces_start", " TeamAlpha", true},
42-
{"consecutive_spaces_end", "TeamAlpha ", true},
40+
{"consecutive_spaces_middle", "team alpha", true},
41+
{"consecutive_spaces_start", " teamalpha", true},
42+
{"consecutive_spaces_end", "teamalpha ", true},
43+
44+
// ❌ Uppercase letters
45+
{"uppercase_letters", "TeamAlpha", true},
4346

4447
// ✅ Borderline valid
45-
{"single_char", "T", false},
46-
{"max_typical", "Alpha Team 2025 - Squad_01", false},
48+
{"single_char", "t", false},
49+
{"max_typical", "alpha team 2025 - squad_01", false},
4750
}
4851

4952
for _, tc := range tests {

0 commit comments

Comments
 (0)