diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 846e1ae058598..6d47d21ad4597 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14976,7 +14976,9 @@ FROM WHERE organization_id = $1 AND - name = $2 + -- Match group name case-insensitively, consistent with organization and + -- workspace name lookups. + LOWER("name") = LOWER($2) LIMIT 1 ` diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index 39742d55350f1..da687d7d1ee15 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -32,9 +32,11 @@ SELECT FROM groups WHERE - organization_id = $1 + organization_id = @organization_id AND - name = $2 + -- Match group name case-insensitively, consistent with organization and + -- workspace name lookups. + LOWER("name") = LOWER(@name) LIMIT 1; diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 76deab7398119..77299416ef084 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -416,6 +416,27 @@ func TestSearchWorkspace(t *testing.T) { SharedWithGroupID: uuid.MustParse("3c831688-0a5a-45a2-a796-f7648874df34"), }, }, + { + Name: "SharedWithGroupInOrgMixedCase", + // The parser lowercases the whole query, so a group whose stored + // name contains uppercase letters must still resolve. See + // GetGroupByOrgAndName, which matches the name case-insensitively. + Query: "shared_with_group:wibble/SupportShare", + Setup: func(t *testing.T, db database.Store) { + org := dbgen.Organization(t, db, database.Organization{ + ID: uuid.MustParse("b5f9d1f4-6d0e-4f6a-9a4a-7b2c3d4e5f60"), + Name: "wibble", + }) + dbgen.Group(t, db, database.Group{ + ID: uuid.MustParse("1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d"), + Name: "SupportShare", + OrganizationID: org.ID, + }) + }, + Expected: database.GetWorkspacesParams{ + SharedWithGroupID: uuid.MustParse("1a2b3c4d-5e6f-4a1b-8c2d-3e4f5a6b7c8d"), + }, + }, { Name: "SharedWithGroupID", Query: "shared_with_group:a7d1ba00-53c7-4aa6-92ea-83157dd57480", diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 95b238f41af5e..be7d3b42229b7 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -199,11 +199,14 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { } if req.Name != "" && req.Name != group.Name { - _, err := api.Database.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{ + existing, err := api.Database.GetGroupByOrgAndName(ctx, database.GetGroupByOrgAndNameParams{ OrganizationID: group.OrganizationID, Name: req.Name, }) - if err == nil { + // GetGroupByOrgAndName matches names case-insensitively, so exclude the + // group being renamed. This allows changing only the casing of a name + // while still rejecting a name already taken by a different group. + if err == nil && existing.ID != group.ID { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: fmt.Sprintf("A group with name %q already exists.", req.Name), }) diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 59335e91c5787..8ccbe81b165d9 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -242,6 +242,31 @@ func TestPatchGroup(t *testing.T) { require.Equal(t, "hi", group.Name) }) + t.Run("RenameCasingOnlyOK", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }}) + userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleUserAdmin()) + ctx := testutil.Context(t, testutil.WaitLong) + group, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "supportshare", + }) + require.NoError(t, err) + + // GetGroupByOrgAndName now matches names case-insensitively, so the + // conflict check must exclude the group being renamed. Otherwise it + // would find itself and reject a rename that only changes casing. + group, err = userAdminClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ + Name: "SupportShare", + }) + require.NoError(t, err) + require.Equal(t, "SupportShare", group.Name) + }) + t.Run("AddUsers", func(t *testing.T) { t.Parallel()