mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add new group members endpoint with filtering and pagination (#23067)
Partially addresses #21813 (still need to make changes to the "add user" button to be complete) Since there are a lot of user tests already, I moved them into `coderdtest` to be shared.
This commit is contained in:
@@ -461,6 +461,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
)
|
||||
|
||||
r.Get("/", api.groupByOrganization)
|
||||
r.Get("/members", api.groupMembersByOrganization)
|
||||
})
|
||||
})
|
||||
r.Route("/provisionerkeys", func(r chi.Router) {
|
||||
@@ -545,6 +546,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
r.Get("/", api.group)
|
||||
r.Patch("/", api.patchGroup)
|
||||
r.Delete("/", api.deleteGroup)
|
||||
r.Get("/members", api.groupMembers)
|
||||
})
|
||||
})
|
||||
r.Route("/workspace-quota", func(r chi.Router) {
|
||||
|
||||
+104
-7
@@ -5,15 +5,18 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
agpl "github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/searchquery"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -393,6 +396,7 @@ func (api *API) groupByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param group path string true "Group id"
|
||||
// @Param exclude_members query bool false "Exclude members from the response"
|
||||
// @Success 200 {object} codersdk.Group
|
||||
// @Router /groups/{group} [get]
|
||||
func (api *API) group(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -401,18 +405,23 @@ func (api *API) group(rw http.ResponseWriter, r *http.Request) {
|
||||
group = httpmw.GroupParam(r)
|
||||
)
|
||||
|
||||
excludeMembers, _ := strconv.ParseBool(r.URL.Query().Get("exclude_members"))
|
||||
|
||||
org, err := api.Database.GetOrganizationByID(ctx, group.OrganizationID)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
}
|
||||
|
||||
users, err := api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{
|
||||
GroupID: group.ID,
|
||||
IncludeSystem: false,
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
users := []database.GroupMember{}
|
||||
if !excludeMembers {
|
||||
users, err = api.Database.GetGroupMembersByGroupID(ctx, database.GetGroupMembersByGroupIDParams{
|
||||
GroupID: group.ID,
|
||||
IncludeSystem: false,
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
memberCount, err := api.Database.GetGroupMembersCountByGroupID(ctx, database.GetGroupMembersCountByGroupIDParams{
|
||||
@@ -431,6 +440,94 @@ func (api *API) group(rw http.ResponseWriter, r *http.Request) {
|
||||
}, users, int(memberCount)))
|
||||
}
|
||||
|
||||
// @Summary Get group members by organization and group name
|
||||
// @ID get-group-members-by-organization-and-group-name
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param organization path string true "Organization ID" format(uuid)
|
||||
// @Param groupName path string true "Group name"
|
||||
// @Param q query string false "Member search query"
|
||||
// @Param after_id query string false "After ID" format(uuid)
|
||||
// @Param limit query int false "Page limit"
|
||||
// @Param offset query int false "Page offset"
|
||||
// @Success 200 {object} codersdk.GroupMembersResponse
|
||||
// @Router /organizations/{organization}/groups/{groupName}/members [get]
|
||||
func (api *API) groupMembersByOrganization(rw http.ResponseWriter, r *http.Request) {
|
||||
api.groupMembers(rw, r)
|
||||
}
|
||||
|
||||
// @Summary Get group members by group ID
|
||||
// @ID get-group-members-by-group-id
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param group path string true "Group id"
|
||||
// @Param q query string false "Member search query"
|
||||
// @Param after_id query string false "After ID" format(uuid)
|
||||
// @Param limit query int false "Page limit"
|
||||
// @Param offset query int false "Page offset"
|
||||
// @Success 200 {object} codersdk.GroupMembersResponse
|
||||
// @Router /groups/{group}/members [get]
|
||||
func (api *API) groupMembers(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
group = httpmw.GroupParam(r)
|
||||
)
|
||||
|
||||
filterQuery := r.URL.Query().Get("q")
|
||||
userFilterParams, filterErrs := searchquery.Users(filterQuery)
|
||||
if len(filterErrs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid member search query.",
|
||||
Validations: filterErrs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
paginationParams, ok := agpl.ParsePagination(rw, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
members, err := api.Database.GetGroupMembersByGroupIDPaginated(ctx, database.GetGroupMembersByGroupIDPaginatedParams{
|
||||
AfterID: paginationParams.AfterID,
|
||||
GroupID: group.ID,
|
||||
IncludeSystem: false,
|
||||
Search: userFilterParams.Search,
|
||||
Name: userFilterParams.Name,
|
||||
Status: userFilterParams.Status,
|
||||
RbacRole: userFilterParams.RbacRole,
|
||||
LastSeenBefore: userFilterParams.LastSeenBefore,
|
||||
LastSeenAfter: userFilterParams.LastSeenAfter,
|
||||
CreatedAfter: userFilterParams.CreatedAfter,
|
||||
CreatedBefore: userFilterParams.CreatedBefore,
|
||||
GithubComUserID: userFilterParams.GithubComUserID,
|
||||
LoginType: userFilterParams.LoginType,
|
||||
// #nosec G115 - Pagination offsets are small and fit in int32
|
||||
OffsetOpt: int32(paginationParams.Offset),
|
||||
// #nosec G115 - Pagination limits are small and fit in int32
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupMembersResponse{
|
||||
Users: nil,
|
||||
Count: 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupMembersResponse{
|
||||
Users: db2sdk.ReducedUsersFromGroupMemberRows(members),
|
||||
Count: int(members[0].Count),
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get groups by organization
|
||||
// @ID get-groups-by-organization
|
||||
// @Security CoderSessionToken
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sort"
|
||||
"testing"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
@@ -584,7 +586,7 @@ func TestPatchGroup(t *testing.T) {
|
||||
userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleUserAdmin())
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := userAdminClient.Group(ctx, user.OrganizationID)
|
||||
group, err := userAdminClient.Group(ctx, user.OrganizationID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 0, group.QuotaAllowance)
|
||||
@@ -636,7 +638,7 @@ func TestGroup(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ggroup, err := userAdminClient.Group(ctx, group.ID)
|
||||
ggroup, err := userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, group, ggroup)
|
||||
})
|
||||
@@ -686,7 +688,7 @@ func TestGroup(t *testing.T) {
|
||||
require.Contains(t, group.Members, user2.ReducedUser)
|
||||
require.Contains(t, group.Members, user3.ReducedUser)
|
||||
|
||||
ggroup, err := userAdminClient.Group(ctx, group.ID)
|
||||
ggroup, err := userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
normalizeGroupMembers(&group)
|
||||
normalizeGroupMembers(&ggroup)
|
||||
@@ -694,6 +696,38 @@ func TestGroup(t *testing.T) {
|
||||
require.Equal(t, group, ggroup)
|
||||
})
|
||||
|
||||
t.Run("WithoutMembers", 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())
|
||||
_, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
_, user3 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := userAdminClient.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "hi",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
group, err = userAdminClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
|
||||
AddUsers: []string{user2.ID.String(), user3.ID.String()},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, group.Members, user2.ReducedUser)
|
||||
require.Contains(t, group.Members, user3.ReducedUser)
|
||||
|
||||
ggroup, err := userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{
|
||||
ExcludeMembers: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ggroup.Members, 0)
|
||||
})
|
||||
|
||||
t.Run("RegularUserReadGroup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -714,7 +748,7 @@ func TestGroup(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ggroup, err := client1.Group(ctx, group.ID)
|
||||
ggroup, err := client1.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err, "regular users can read groups unless workspace sharing is disabled")
|
||||
normalizeGroupMembers(&group)
|
||||
normalizeGroupMembers(&ggroup)
|
||||
@@ -760,7 +794,7 @@ func TestGroup(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = client1.Group(ctx, group.ID)
|
||||
_, err = client1.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.Error(t, err, "regular users cannot read groups when workspace sharing is disabled")
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
@@ -797,7 +831,7 @@ func TestGroup(t *testing.T) {
|
||||
err = userAdminClient.DeleteUser(ctx, user1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
group, err = userAdminClient.Group(ctx, group.ID)
|
||||
group, err = userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, group.Members, user1.ReducedUser)
|
||||
})
|
||||
@@ -832,7 +866,7 @@ func TestGroup(t *testing.T) {
|
||||
user1, err = userAdminClient.UpdateUserStatus(ctx, user1.ID.String(), codersdk.UserStatusSuspended)
|
||||
require.NoError(t, err)
|
||||
|
||||
group, err = userAdminClient.Group(ctx, group.ID)
|
||||
group, err = userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 2)
|
||||
require.Contains(t, group.Members, user1.ReducedUser)
|
||||
@@ -854,7 +888,7 @@ func TestGroup(t *testing.T) {
|
||||
AddUsers: []string{anotherUser.ID.String()},
|
||||
})
|
||||
|
||||
group, err = userAdminClient.Group(ctx, group.ID)
|
||||
group, err = userAdminClient.Group(ctx, group.ID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 3)
|
||||
require.Contains(t, group.Members, user1.ReducedUser)
|
||||
@@ -916,7 +950,7 @@ func TestGroup(t *testing.T) {
|
||||
prebuildsUser, err := client.User(ctx, database.PrebuildsSystemUserID.String())
|
||||
require.NoError(t, err)
|
||||
// The 'Everyone' group always has an ID that matches the organization ID.
|
||||
group, err := userAdminClient.Group(ctx, user.OrganizationID)
|
||||
group, err := userAdminClient.Group(ctx, user.OrganizationID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, group.Members, 4)
|
||||
require.Equal(t, "Everyone", group.Name)
|
||||
@@ -971,7 +1005,7 @@ func TestGroups(t *testing.T) {
|
||||
normalizeGroupMembers(&group2)
|
||||
|
||||
// Fetch everyone group for comparison
|
||||
everyoneGroup, err := userAdminClient.Group(ctx, user.OrganizationID)
|
||||
everyoneGroup, err := userAdminClient.Group(ctx, user.OrganizationID, codersdk.GroupRequest{})
|
||||
require.NoError(t, err)
|
||||
normalizeGroupMembers(&everyoneGroup)
|
||||
|
||||
@@ -1052,7 +1086,7 @@ func TestDeleteGroup(t *testing.T) {
|
||||
err = userAdminClient.DeleteGroup(ctx, group1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = userAdminClient.Group(ctx, group1.ID)
|
||||
_, err = userAdminClient.Group(ctx, group1.ID, codersdk.GroupRequest{})
|
||||
require.Error(t, err)
|
||||
cerr, ok := codersdk.AsError(err)
|
||||
require.True(t, ok)
|
||||
@@ -1114,3 +1148,87 @@ func TestDeleteGroup(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetGroupMembersFilter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, db, first := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
OIDCConfig: &coderd.OIDCConfig{
|
||||
AllowSignups: true,
|
||||
},
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleUserAdmin())
|
||||
|
||||
setupCtx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
group, err := userAdminClient.CreateGroup(setupCtx, first.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "filtered",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := func(users []codersdk.User) {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, user := range users {
|
||||
userIDs[i] = user.ID.String()
|
||||
}
|
||||
group, err = userAdminClient.PatchGroup(setupCtx, group.ID, codersdk.PatchGroupRequest{
|
||||
AddUsers: userIDs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
fetch := func(testCtx context.Context, req codersdk.UsersRequest) []codersdk.ReducedUser {
|
||||
res, err := userAdminClient.GroupMembers(testCtx, group.ID, req)
|
||||
require.NoError(t, err)
|
||||
return res.Users
|
||||
}
|
||||
coderdtest.UsersFilter(setupCtx, t, client, db, setup, fetch)
|
||||
}
|
||||
|
||||
func TestGetGroupMembersPagination(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, first := coderdenttest.New(t, &coderdenttest.Options{
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
userAdminClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleUserAdmin())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
group, err := userAdminClient.CreateGroup(ctx, first.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "paginated",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := func(users []codersdk.User) {
|
||||
userIDs := make([]string, len(users))
|
||||
for i, user := range users {
|
||||
userIDs[i] = user.ID.String()
|
||||
}
|
||||
group, err = userAdminClient.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{
|
||||
AddUsers: userIDs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
fetch := func(req codersdk.UsersRequest) ([]codersdk.ReducedUser, int) {
|
||||
group, err := userAdminClient.GroupMembers(ctx, group.ID, req)
|
||||
require.NoError(t, err)
|
||||
return group.Users, group.Count
|
||||
}
|
||||
coderdtest.UsersPagination(ctx, t, client, setup, fetch)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user