From c330af0e4dd3a1e45580518e7dd3650dc0c2d758 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 16 Sep 2024 13:01:46 -0500 Subject: [PATCH] chore: add group_ids filter to /groups endpoint (#14688) Allow filtering groups by IDs. --- coderd/apidoc/docs.go | 7 ++++++ coderd/apidoc/swagger.json | 7 ++++++ coderd/database/dbmem/dbmem.go | 6 +++++ coderd/database/queries.sql.go | 18 +++++++++++--- coderd/database/queries/groups.sql | 4 +++ codersdk/groups.go | 11 +++++++++ docs/reference/api/enterprise.md | 11 +++++---- enterprise/coderd/groups.go | 4 +++ enterprise/coderd/groups_test.go | 39 ++++++++++++++++++++++++++++++ site/src/api/typesGenerated.ts | 1 + 10 files changed, 99 insertions(+), 9 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6d2dfc7c83..4af6cde3ea 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1062,6 +1062,13 @@ const docTemplate = `{ "name": "has_member", "in": "query", "required": true + }, + { + "type": "string", + "description": "Comma separated list of group IDs", + "name": "group_ids", + "in": "query", + "required": true } ], "responses": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0bec005a56..5bcd336955 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -916,6 +916,13 @@ "name": "has_member", "in": "query", "required": true + }, + { + "type": "string", + "description": "Comma separated list of group IDs", + "name": "group_ids", + "in": "query", + "required": true } ], "responses": { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ed766d48ec..16b0c89fc8 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -2714,6 +2714,12 @@ func (q *FakeQuerier) GetGroups(_ context.Context, arg database.GetGroupsParams) orgDetailsCache := make(map[uuid.UUID]struct{ name, displayName string }) filtered := make([]database.GetGroupsRow, 0) for _, group := range q.groups { + if len(arg.GroupIds) > 0 { + if !slices.Contains(arg.GroupIds, group.ID) { + continue + } + } + if arg.OrganizationID != uuid.Nil && group.OrganizationID != arg.OrganizationID { continue } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 6831415907..e1e7e9c504 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1683,12 +1683,17 @@ WHERE groups.name = ANY($3) ELSE true END + AND CASE WHEN array_length($4 :: uuid[], 1) > 0 THEN + groups.id = ANY($4) + ELSE true + END ` type GetGroupsParams struct { - OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` - HasMemberID uuid.UUID `db:"has_member_id" json:"has_member_id"` - GroupNames []string `db:"group_names" json:"group_names"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + HasMemberID uuid.UUID `db:"has_member_id" json:"has_member_id"` + GroupNames []string `db:"group_names" json:"group_names"` + GroupIds []uuid.UUID `db:"group_ids" json:"group_ids"` } type GetGroupsRow struct { @@ -1698,7 +1703,12 @@ type GetGroupsRow struct { } func (q *sqlQuerier) GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error) { - rows, err := q.db.QueryContext(ctx, getGroups, arg.OrganizationID, arg.HasMemberID, pq.Array(arg.GroupNames)) + rows, err := q.db.QueryContext(ctx, getGroups, + arg.OrganizationID, + arg.HasMemberID, + pq.Array(arg.GroupNames), + pq.Array(arg.GroupIds), + ) if err != nil { return nil, err } diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index 780c0d0154..48a5ba5c79 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -56,6 +56,10 @@ WHERE groups.name = ANY(@group_names) ELSE true END + AND CASE WHEN array_length(@group_ids :: uuid[], 1) > 0 THEN + groups.id = ANY(@group_ids) + ELSE true + END ; -- name: InsertGroup :one diff --git a/codersdk/groups.go b/codersdk/groups.go index 9b0e487822..26ef7f829f 100644 --- a/codersdk/groups.go +++ b/codersdk/groups.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/google/uuid" "golang.org/x/xerrors" @@ -74,6 +75,9 @@ type GroupArguments struct { Organization string // HasMember can be a user uuid or username HasMember string + // GroupIDs is a list of group UUIDs to filter by. + // If not set, all groups will be returned. + GroupIDs []uuid.UUID } func (c *Client) Groups(ctx context.Context, args GroupArguments) ([]Group, error) { @@ -84,6 +88,13 @@ func (c *Client) Groups(ctx context.Context, args GroupArguments) ([]Group, erro if args.HasMember != "" { qp.Set("has_member", args.HasMember) } + if len(args.GroupIDs) > 0 { + idStrs := make([]string, 0, len(args.GroupIDs)) + for _, id := range args.GroupIDs { + idStrs = append(idStrs, id.String()) + } + qp.Set("group_ids", strings.Join(idStrs, ",")) + } res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/groups?%s", qp.Encode()), diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index 7cf3ad6a1d..ea6f2d96d7 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -179,7 +179,7 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl -curl -X GET http://coder-server:8080/api/v2/groups?organization=string&has_member=string \ +curl -X GET http://coder-server:8080/api/v2/groups?organization=string&has_member=string&group_ids=string \ -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` @@ -188,10 +188,11 @@ curl -X GET http://coder-server:8080/api/v2/groups?organization=string&has_membe ### Parameters -| Name | In | Type | Required | Description | -| -------------- | ----- | ------ | -------- | ----------------------- | -| `organization` | query | string | true | Organization ID or name | -| `has_member` | query | string | true | User ID or name | +| Name | In | Type | Required | Description | +| -------------- | ----- | ------ | -------- | --------------------------------- | +| `organization` | query | string | true | Organization ID or name | +| `has_member` | query | string | true | User ID or name | +| `group_ids` | query | string | true | Comma separated list of group IDs | ### Example responses diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 4f58fb429b..8d5a7fceef 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -430,6 +430,7 @@ func (api *API) groupsByOrganization(rw http.ResponseWriter, r *http.Request) { // @Tags Enterprise // @Param organization query string true "Organization ID or name" // @Param has_member query string true "User ID or name" +// @Param group_ids query string true "Comma separated list of group IDs" // @Success 200 {array} codersdk.Group // @Router /groups [get] func (api *API) groups(rw http.ResponseWriter, r *http.Request) { @@ -457,6 +458,9 @@ func (api *API) groups(rw http.ResponseWriter, r *http.Request) { } return user.ID, nil }) + + filter.GroupIds = parser.UUIDs(r.URL.Query(), []uuid.UUID{}, "group_ids") + parser.ErrorExcessParams(r.URL.Query()) if len(parser.Errors) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index fc404e3da1..1baf62211d 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -779,6 +779,45 @@ func TestGroup(t *testing.T) { require.Contains(t, group.Members, user2.ReducedUser) }) + t.Run("ByIDs", 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) + groupA, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "group-a", + }) + require.NoError(t, err) + + groupB, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "group-b", + }) + require.NoError(t, err) + + // group-c should be omitted from the filter + _, err = userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "group-c", + }) + require.NoError(t, err) + + found, err := userAdminClient.Groups(ctx, codersdk.GroupArguments{ + GroupIDs: []uuid.UUID{groupA.ID, groupB.ID}, + }) + require.NoError(t, err) + + foundIDs := db2sdk.List(found, func(g codersdk.Group) uuid.UUID { + return g.ID + }) + + require.ElementsMatch(t, []uuid.UUID{groupA.ID, groupB.ID}, foundIDs) + }) + t.Run("everyoneGroupReturnsEmpty", func(t *testing.T) { t.Parallel() diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 6b95096a2a..9372366227 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -631,6 +631,7 @@ export interface Group { export interface GroupArguments { readonly Organization: string; readonly HasMember: string; + readonly GroupIDs: Readonly>; } // From codersdk/idpsync.go