From cea4d3cfdc3d14e6f9959d4cb3ce1380fe35dae0 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 17:43:03 +0000 Subject: [PATCH 01/18] feat: add paginated members route --- coderd/coderd.go | 3 +++ coderd/members.go | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/coderd/coderd.go b/coderd/coderd.go index ab8e99d29dea8..ec8499ab86e60 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1002,6 +1002,9 @@ func New(options *Options) *API { }) }) }) + r.Route("/paginated-members", func(r chi.Router) { + r.Get("/", api.paginatedMembers) + }) r.Route("/members", func(r chi.Router) { r.Get("/", api.listMembers) r.Route("/roles", func(r chi.Router) { diff --git a/coderd/members.go b/coderd/members.go index c89b4c9c09c1a..ed78304a0ec9a 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -178,6 +178,19 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } +// @Summary Paginated organization members +// @ID paginated-organization-members +// @Security CoderSessionToken +// @Produce json +// @Tags Members +// @Param organization path string true "Organization ID" +// @Success 200 {object} []codersdk.OrganizationMemberWithUserData +// @Router /organizations/{organization}/paginated-members [get] +func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + httpapi.Write(ctx, rw, http.StatusNotImplemented, nil) +} + // @Summary Assign role to organization member // @ID assign-role-to-organization-member // @Security CoderSessionToken From e97c9dbd88754f8c38e00f2c023c5f5c9b8c55da Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 18:45:40 +0000 Subject: [PATCH 02/18] feat: add request and response types --- coderd/members.go | 4 +++- codersdk/organizations.go | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/coderd/members.go b/coderd/members.go index ed78304a0ec9a..29722834dbe07 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -188,7 +188,9 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Router /organizations/{organization}/paginated-members [get] func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - httpapi.Write(ctx, rw, http.StatusNotImplemented, nil) + + resp := codersdk.PaginatedMembersResponse{} + httpapi.Write(ctx, rw, http.StatusOK, resp) } // @Summary Assign role to organization member diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 781baaaa5d5d6..2c1f9dc986b7c 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -81,6 +81,22 @@ type OrganizationMemberWithUserData struct { OrganizationMember `table:"m,recursive_inline"` } +type PaginatedMembersRequest struct { + // + OrganizationID uuid.UUID `table:"organization id" json:"organization_id" format:"uuid"` + // + Limit int `json:"limit,omitempty"` + // + Offset int `json:"offset,omitempty"` +} + +type PaginatedMembersResponse struct { + // + Members []OrganizationMemberWithUserData + // + Count int `json:"count"` +} + type CreateOrganizationRequest struct { Name string `json:"name" validate:"required,organization_name"` // DisplayName will default to the same value as `Name` if not provided. From 34003dcd1edd17530e2934919766f8bcffecbfba Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 19:07:28 +0000 Subject: [PATCH 03/18] feat: create new sql query and generate code --- coderd/apidoc/docs.go | 37 +++++++++ coderd/apidoc/swagger.json | 33 ++++++++ coderd/database/dbauthz/dbauthz.go | 4 + coderd/database/dbmem/dbmem.go | 9 +++ coderd/database/dbmetrics/querymetrics.go | 7 ++ coderd/database/dbmock/dbmock.go | 15 ++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 75 +++++++++++++++++ .../database/queries/organizationmembers.sql | 23 ++++++ docs/reference/api/members.md | 81 +++++++++++++++++++ site/src/api/typesGenerated.ts | 13 +++ 11 files changed, 298 insertions(+) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8f90cd5c205a2..15b087f819e2e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2971,6 +2971,43 @@ const docTemplate = `{ } } }, + "/organizations/{organization}/paginated-members": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Members" + ], + "summary": "Paginated organization members", + "operationId": "paginated-organization-members", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + } + } + } + } + } + }, "/organizations/{organization}/provisionerdaemons": { "get": { "security": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index fcfe56d3fc4aa..df1f58359fced 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2607,6 +2607,39 @@ } } }, + "/organizations/{organization}/paginated-members": { + "get": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Members"], + "summary": "Paginated organization members", + "operationId": "paginated-organization-members", + "parameters": [ + { + "type": "string", + "description": "Organization ID", + "name": "organization", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + } + } + } + } + } + }, "/organizations/{organization}/provisionerdaemons": { "get": { "security": [ diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index a4d76fa0198ed..75c6524e4605f 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3581,6 +3581,10 @@ func (q *querier) OrganizationMembers(ctx context.Context, arg database.Organiza return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.OrganizationMembers)(ctx, arg) } +func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { + panic("not implemented") +} + func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { template, err := q.db.GetTemplateByID(ctx, templateID) if err != nil { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7f7ff987ff544..5ad0f23572e03 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9584,6 +9584,15 @@ func (q *FakeQuerier) OrganizationMembers(_ context.Context, arg database.Organi return tmp, nil } +func (q *FakeQuerier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { + err := validateDatabaseType(arg) + if err != nil { + return nil, err + } + + panic("not implemented") +} + func (q *FakeQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(_ context.Context, templateID uuid.UUID) error { err := validateDatabaseType(templateID) if err != nil { diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 0d021f978151b..407d9e48bfcf8 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -2278,6 +2278,13 @@ func (m queryMetricsStore) OrganizationMembers(ctx context.Context, arg database return r0, r1 } +func (m queryMetricsStore) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { + start := time.Now() + r0, r1 := m.s.PaginatedOrganizationMembers(ctx, arg) + m.queryLatencies.WithLabelValues("PaginatedOrganizationMembers").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m queryMetricsStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { start := time.Now() r0 := m.s.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 6e07614f4cb3f..fbe4d0745fbb0 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -4823,6 +4823,21 @@ func (mr *MockStoreMockRecorder) PGLocks(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PGLocks", reflect.TypeOf((*MockStore)(nil).PGLocks), ctx) } +// PaginatedOrganizationMembers mocks base method. +func (m *MockStore) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaginatedOrganizationMembers", ctx, arg) + ret0, _ := ret[0].([]database.PaginatedOrganizationMembersRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaginatedOrganizationMembers indicates an expected call of PaginatedOrganizationMembers. +func (mr *MockStoreMockRecorder) PaginatedOrganizationMembers(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaginatedOrganizationMembers", reflect.TypeOf((*MockStore)(nil).PaginatedOrganizationMembers), ctx, arg) +} + // Ping mocks base method. func (m *MockStore) Ping(ctx context.Context) (time.Duration, error) { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 28227797c7e3f..d72469650f0ea 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -478,6 +478,7 @@ type sqlcQuerier interface { // - Use just 'user_id' to get all orgs a user is a member of // - Use both to get a specific org member row OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error) + PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error) RemoveUserFromAllGroups(ctx context.Context, userID uuid.UUID) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2d38ab38b0f25..433eef67c7cc7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5270,6 +5270,81 @@ func (q *sqlQuerier) OrganizationMembers(ctx context.Context, arg OrganizationMe return items, nil } +const paginatedOrganizationMembers = `-- name: PaginatedOrganizationMembers :many +SELECT + organization_members.user_id, organization_members.organization_id, organization_members.created_at, organization_members.updated_at, organization_members.roles, + users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", + COUNT(*) OVER() AS count +FROM + organization_members + INNER JOIN + users ON organization_members.user_id = users.id AND users.deleted = false +WHERE + -- Filter by organization id + CASE + WHEN $1 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = $1 + ELSE true + END +ORDER BY + -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. + LOWER(username) ASC OFFSET $2 +LIMIT + -- A null limit means "no limit", so 0 means return all + NULLIF($3 :: int, 0) +` + +type PaginatedOrganizationMembersParams struct { + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` + OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` + LimitOpt int32 `db:"limit_opt" json:"limit_opt"` +} + +type PaginatedOrganizationMembersRow struct { + OrganizationMember OrganizationMember `db:"organization_member" json:"organization_member"` + Username string `db:"username" json:"username"` + AvatarURL string `db:"avatar_url" json:"avatar_url"` + Name string `db:"name" json:"name"` + Email string `db:"email" json:"email"` + GlobalRoles pq.StringArray `db:"global_roles" json:"global_roles"` + Count int64 `db:"count" json:"count"` +} + +func (q *sqlQuerier) PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error) { + rows, err := q.db.QueryContext(ctx, paginatedOrganizationMembers, arg.OrganizationID, arg.OffsetOpt, arg.LimitOpt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PaginatedOrganizationMembersRow + for rows.Next() { + var i PaginatedOrganizationMembersRow + if err := rows.Scan( + &i.OrganizationMember.UserID, + &i.OrganizationMember.OrganizationID, + &i.OrganizationMember.CreatedAt, + &i.OrganizationMember.UpdatedAt, + pq.Array(&i.OrganizationMember.Roles), + &i.Username, + &i.AvatarURL, + &i.Name, + &i.Email, + &i.GlobalRoles, + &i.Count, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateMemberRoles = `-- name: UpdateMemberRoles :one UPDATE organization_members diff --git a/coderd/database/queries/organizationmembers.sql b/coderd/database/queries/organizationmembers.sql index 71304c8883602..9e603ded03526 100644 --- a/coderd/database/queries/organizationmembers.sql +++ b/coderd/database/queries/organizationmembers.sql @@ -66,3 +66,26 @@ WHERE user_id = @user_id AND organization_id = @org_id RETURNING *; + +-- name: PaginatedOrganizationMembers :many +SELECT + sqlc.embed(organization_members), + users.username, users.avatar_url, users.name, users.email, users.rbac_roles as "global_roles", + COUNT(*) OVER() AS count +FROM + organization_members + INNER JOIN + users ON organization_members.user_id = users.id AND users.deleted = false +WHERE + -- Filter by organization id + CASE + WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + organization_id = @organization_id + ELSE true + END +ORDER BY + -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. + LOWER(username) ASC OFFSET @offset_opt +LIMIT + -- A null limit means "no limit", so 0 means return all + NULLIF(@limit_opt :: int, 0); diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 5dc39cee2d088..11b0f1c50cc86 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -813,6 +813,87 @@ curl -X PUT http://coder-server:8080/api/v2/organizations/{organization}/members To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Paginated organization members + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginated-members \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`GET /organizations/{organization}/paginated-members` + +### Parameters + +| Name | In | Type | Required | Description | +|----------------|------|--------|----------|-----------------| +| `organization` | path | string | true | Organization ID | + +### Example responses + +> 200 Response + +```json +[ + { + "avatar_url": "string", + "created_at": "2019-08-24T14:15:22Z", + "email": "string", + "global_roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" + } +] +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OrganizationMemberWithUserData](schemas.md#codersdkorganizationmemberwithuserdata) | + +

Response Schema

+ +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +|----------------------|-------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» avatar_url` | string | false | | | +| `» created_at` | string(date-time) | false | | | +| `» email` | string | false | | | +| `» global_roles` | array | false | | | +| `»» display_name` | string | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string | false | | | +| `» name` | string | false | | | +| `» organization_id` | string(uuid) | false | | | +| `» roles` | array | false | | | +| `» updated_at` | string(date-time) | false | | | +| `» user_id` | string(uuid) | false | | | +| `» username` | string | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Get site member roles ### Code samples diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 222c07575b969..6fdfb5ea9d9a1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1484,6 +1484,19 @@ export interface OrganizationSyncSettings { readonly organization_assign_default: boolean; } +// From codersdk/organizations.go +export interface PaginatedMembersRequest { + readonly organization_id: string; + readonly limit?: number; + readonly offset?: number; +} + +// From codersdk/organizations.go +export interface PaginatedMembersResponse { + readonly Members: readonly OrganizationMemberWithUserData[]; + readonly count: number; +} + // From codersdk/pagination.go export interface Pagination { readonly after_id?: string; From 9edb0887fa73b92920a53490bf398bd02cd95bda Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 21:21:37 +0000 Subject: [PATCH 04/18] fix: add dbauthz tests --- coderd/database/dbauthz/dbauthz.go | 2 +- coderd/database/dbauthz/dbauthz_test.go | 16 +++++++++++++ coderd/database/dbmem/dbmem.go | 32 ++++++++++++++++++++++++- coderd/database/modelmethods.go | 4 ++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 75c6524e4605f..d567285a47219 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3582,7 +3582,7 @@ func (q *querier) OrganizationMembers(ctx context.Context, arg database.Organiza } func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { - panic("not implemented") + return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.PaginatedOrganizationMembers)(ctx, arg) } func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 614a357efcbc5..575744727ae60 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -985,6 +985,22 @@ func (s *MethodTestSuite) TestOrganization() { mem, policy.ActionRead, ) })) + s.Run("PaginatedOrganizationMembers", s.Subtest(func(db database.Store, check *expects) { + o := dbgen.Organization(s.T(), db, database.Organization{}) + u := dbgen.User(s.T(), db, database.User{}) + mem := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{ + OrganizationID: o.ID, + UserID: u.ID, + Roles: []string{rbac.RoleOrgAdmin()}, + }) + + check.Args(database.PaginatedOrganizationMembersParams{ + OrganizationID: uuid.UUID{}, + LimitOpt: 1, + }).Asserts( + mem, policy.ActionRead, + ) + })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5ad0f23572e03..07e3c38212bf3 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9590,7 +9590,37 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(ctx context.Context, arg data return nil, err } - panic("not implemented") + q.mutex.RLock() + defer q.mutex.RUnlock() + + tmp := make([]database.PaginatedOrganizationMembersRow, 0) + + skippedMembers := 0 + for _, organizationMember := range q.organizationMembers { + if arg.OrganizationID != uuid.Nil && organizationMember.OrganizationID != arg.OrganizationID { + continue + } + + if skippedMembers < int(arg.OffsetOpt) { + skippedMembers += 1 + continue + } + + if len(tmp) >= int(arg.LimitOpt) { + break + } + + user, _ := q.getUserByIDNoLock(organizationMember.UserID) + tmp = append(tmp, database.PaginatedOrganizationMembersRow{ + OrganizationMember: organizationMember, + Username: user.Username, + AvatarURL: user.AvatarURL, + Name: user.Name, + Email: user.Email, + GlobalRoles: user.RBACRoles, + }) + } + return tmp, nil } func (q *FakeQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(_ context.Context, templateID uuid.UUID) error { diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index fe782bdd14170..a9dbc3e530994 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -256,6 +256,10 @@ func (m OrganizationMembersRow) RBACObject() rbac.Object { return m.OrganizationMember.RBACObject() } +func (m PaginatedOrganizationMembersRow) RBACObject() rbac.Object { + return m.OrganizationMember.RBACObject() +} + func (m GetOrganizationIDsByMemberIDsRow) RBACObject() rbac.Object { // TODO: This feels incorrect as we are really returning a list of orgmembers. // This return type should be refactored to return a list of orgmembers, not this From ef40967960953e7d36bbeec48abaeae2c6f0d667 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 22:08:05 +0000 Subject: [PATCH 05/18] feat: implement endpoint --- coderd/members.go | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/coderd/members.go b/coderd/members.go index 29722834dbe07..3a2ddc5ea2cfc 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -187,9 +187,49 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Success 200 {object} []codersdk.OrganizationMemberWithUserData // @Router /organizations/{organization}/paginated-members [get] func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() + var ( + ctx = r.Context() + organization = httpmw.OrganizationParam(r) + paginationParams, ok = parsePagination(rw, r) + ) + if !ok { + return + } - resp := codersdk.PaginatedMembersResponse{} + paginatedMemberRows, err := api.Database.PaginatedOrganizationMembers(ctx, database.PaginatedOrganizationMembersParams{ + OrganizationID: organization.ID, + LimitOpt: int32(paginationParams.Limit), + OffsetOpt: int32(paginationParams.Offset), + }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if err != nil { + httpapi.InternalServerError(rw, err) + return + } + + memberRows := make([]database.OrganizationMembersRow, len(paginatedMemberRows)) + for _, pRow := range paginatedMemberRows { + row := database.OrganizationMembersRow{ + OrganizationMember: pRow.OrganizationMember, + Username: pRow.Username, + AvatarURL: pRow.AvatarURL, + Name: pRow.Name, + Email: pRow.Email, + GlobalRoles: pRow.GlobalRoles, + } + + memberRows = append(memberRows, row) + } + + members, err := convertOrganizationMembersWithUserData(ctx, api.Database, memberRows) + + resp := codersdk.PaginatedMembersResponse{ + Members: members, + Count: int(paginatedMemberRows[0].Count), + } httpapi.Write(ctx, rw, http.StatusOK, resp) } From 6aaddaf58b48a0a4b0143d46b76970838b1218b1 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 22:15:55 +0000 Subject: [PATCH 06/18] fix: correct endpoint documentation --- coderd/members.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/coderd/members.go b/coderd/members.go index 3a2ddc5ea2cfc..d71eb69d08a75 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -184,7 +184,9 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Members // @Param organization path string true "Organization ID" -// @Success 200 {object} []codersdk.OrganizationMemberWithUserData +// @Param limit query int false "Page limit" +// @Param offset query int false "Page offset" +// @Success 200 {object} []codersdk.codersdk.PaginatedMembersResponse // @Router /organizations/{organization}/paginated-members [get] func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { var ( From a724f484c93e201e8d6ae95b5f9bf2b330971c4b Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 22:25:00 +0000 Subject: [PATCH 07/18] fix: lint errors --- coderd/database/dbmem/dbmem.go | 4 ++-- coderd/members.go | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 07e3c38212bf3..d0329e3c3444f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9584,7 +9584,7 @@ func (q *FakeQuerier) OrganizationMembers(_ context.Context, arg database.Organi return tmp, nil } -func (q *FakeQuerier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { +func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { err := validateDatabaseType(arg) if err != nil { return nil, err @@ -9602,7 +9602,7 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(ctx context.Context, arg data } if skippedMembers < int(arg.OffsetOpt) { - skippedMembers += 1 + skippedMembers++ continue } diff --git a/coderd/members.go b/coderd/members.go index d71eb69d08a75..977b23a867054 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -212,7 +212,7 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { return } - memberRows := make([]database.OrganizationMembersRow, len(paginatedMemberRows)) + memberRows := make([]database.OrganizationMembersRow, 0) for _, pRow := range paginatedMemberRows { row := database.OrganizationMembersRow{ OrganizationMember: pRow.OrganizationMember, @@ -227,6 +227,9 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { } members, err := convertOrganizationMembersWithUserData(ctx, api.Database, memberRows) + if err != nil { + httpapi.InternalServerError(rw, err) + } resp := codersdk.PaginatedMembersResponse{ Members: members, From 49255925dabccce0ad463c91a70f252f3b1f53b5 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 22:40:11 +0000 Subject: [PATCH 08/18] fix: generated docs from new endpoint --- coderd/apidoc/docs.go | 28 ++++++++++- coderd/apidoc/swagger.json | 28 ++++++++++- coderd/members.go | 2 +- docs/reference/api/members.md | 91 +++++++++++++++++++---------------- docs/reference/api/schemas.md | 41 ++++++++++++++++ 5 files changed, 146 insertions(+), 44 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 15b087f819e2e..26ecfdbc70180 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2993,6 +2993,18 @@ const docTemplate = `{ "name": "organization", "in": "path", "required": true + }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page offset", + "name": "offset", + "in": "query" } ], "responses": { @@ -3001,7 +3013,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + "$ref": "#/definitions/codersdk.PaginatedMembersResponse" } } } @@ -12939,6 +12951,20 @@ const docTemplate = `{ } } }, + "codersdk.PaginatedMembersResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + } + } + } + }, "codersdk.PatchGroupIDPSyncConfigRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index df1f58359fced..8b9f538735c99 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2625,6 +2625,18 @@ "name": "organization", "in": "path", "required": true + }, + { + "type": "integer", + "description": "Page limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page offset", + "name": "offset", + "in": "query" } ], "responses": { @@ -2633,7 +2645,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + "$ref": "#/definitions/codersdk.PaginatedMembersResponse" } } } @@ -11662,6 +11674,20 @@ } } }, + "codersdk.PaginatedMembersResponse": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.OrganizationMemberWithUserData" + } + } + } + }, "codersdk.PatchGroupIDPSyncConfigRequest": { "type": "object", "properties": { diff --git a/coderd/members.go b/coderd/members.go index 977b23a867054..0b2fd7b5757b8 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -186,7 +186,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Param organization path string true "Organization ID" // @Param limit query int false "Page limit" // @Param offset query int false "Page offset" -// @Success 200 {object} []codersdk.codersdk.PaginatedMembersResponse +// @Success 200 {object} []codersdk.PaginatedMembersResponse // @Router /organizations/{organization}/paginated-members [get] func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) { var ( diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 11b0f1c50cc86..882323083a2ce 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -828,9 +828,11 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat ### Parameters -| Name | In | Type | Required | Description | -|----------------|------|--------|----------|-----------------| -| `organization` | path | string | true | Organization ID | +| Name | In | Type | Required | Description | +|----------------|-------|---------|----------|-----------------| +| `organization` | path | string | true | Organization ID | +| `limit` | query | integer | false | Page limit | +| `offset` | query | integer | false | Page offset | ### Example responses @@ -839,58 +841,65 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat ```json [ { - "avatar_url": "string", - "created_at": "2019-08-24T14:15:22Z", - "email": "string", - "global_roles": [ + "count": 0, + "members": [ { - "display_name": "string", + "avatar_url": "string", + "created_at": "2019-08-24T14:15:22Z", + "email": "string", + "global_roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], "name": "string", - "organization_id": "string" + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" } - ], - "name": "string", - "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", - "roles": [ - { - "display_name": "string", - "name": "string", - "organization_id": "string" - } - ], - "updated_at": "2019-08-24T14:15:22Z", - "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", - "username": "string" + ] } ] ``` ### Responses -| Status | Meaning | Description | Schema | -|--------|---------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------| -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.OrganizationMemberWithUserData](schemas.md#codersdkorganizationmemberwithuserdata) | +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.PaginatedMembersResponse](schemas.md#codersdkpaginatedmembersresponse) |

Response Schema

Status Code **200** -| Name | Type | Required | Restrictions | Description | -|----------------------|-------------------|----------|--------------|-------------| -| `[array item]` | array | false | | | -| `» avatar_url` | string | false | | | -| `» created_at` | string(date-time) | false | | | -| `» email` | string | false | | | -| `» global_roles` | array | false | | | -| `»» display_name` | string | false | | | -| `»» name` | string | false | | | -| `»» organization_id` | string | false | | | -| `» name` | string | false | | | -| `» organization_id` | string(uuid) | false | | | -| `» roles` | array | false | | | -| `» updated_at` | string(date-time) | false | | | -| `» user_id` | string(uuid) | false | | | -| `» username` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|-----------------------|-------------------|----------|--------------|-------------| +| `[array item]` | array | false | | | +| `» count` | integer | false | | | +| `» members` | array | false | | | +| `»» avatar_url` | string | false | | | +| `»» created_at` | string(date-time) | false | | | +| `»» email` | string | false | | | +| `»» global_roles` | array | false | | | +| `»»» display_name` | string | false | | | +| `»»» name` | string | false | | | +| `»»» organization_id` | string | false | | | +| `»» name` | string | false | | | +| `»» organization_id` | string(uuid) | false | | | +| `»» roles` | array | false | | | +| `»» updated_at` | string(date-time) | false | | | +| `»» user_id` | string(uuid) | false | | | +| `»» username` | string | false | | | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 9fa22af7356ae..42ef8a7ade184 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -4189,6 +4189,47 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | » `[any property]` | array of string | false | | | | `organization_assign_default` | boolean | false | | Organization assign default will ensure the default org is always included for every user, regardless of their claims. This preserves legacy behavior. | +## codersdk.PaginatedMembersResponse + +```json +{ + "count": 0, + "members": [ + { + "avatar_url": "string", + "created_at": "2019-08-24T14:15:22Z", + "email": "string", + "global_roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "name": "string", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", + "roles": [ + { + "display_name": "string", + "name": "string", + "organization_id": "string" + } + ], + "updated_at": "2019-08-24T14:15:22Z", + "user_id": "a169451c-8525-4352-b8ca-070dd449a1a5", + "username": "string" + } + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|-----------|---------------------------------------------------------------------------------------------|----------|--------------|-------------| +| `count` | integer | false | | | +| `members` | array of [codersdk.OrganizationMemberWithUserData](#codersdkorganizationmemberwithuserdata) | false | | | + ## codersdk.PatchGroupIDPSyncConfigRequest ```json From 902538c5c387f8fe05cc8d801ba4a6a16540991d Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Thu, 6 Mar 2025 22:44:47 +0000 Subject: [PATCH 09/18] fix: remove unneeded comments --- codersdk/organizations.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 2c1f9dc986b7c..e093f6f85594a 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -82,19 +82,14 @@ type OrganizationMemberWithUserData struct { } type PaginatedMembersRequest struct { - // OrganizationID uuid.UUID `table:"organization id" json:"organization_id" format:"uuid"` - // - Limit int `json:"limit,omitempty"` - // - Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` } type PaginatedMembersResponse struct { - // Members []OrganizationMemberWithUserData - // - Count int `json:"count"` + Count int `json:"count"` } type CreateOrganizationRequest struct { From 06ff95c196c94d000f85840757128fd72a9f0744 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 7 Mar 2025 20:46:50 +0000 Subject: [PATCH 10/18] wip --- coderd/database/dbauthz/dbauthz.go | 5 ++++- coderd/database/dbauthz/dbauthz_test.go | 10 ++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index d567285a47219..19e10ca0f1f78 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3582,7 +3582,10 @@ func (q *querier) OrganizationMembers(ctx context.Context, arg database.Organiza } func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { - return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.PaginatedOrganizationMembers)(ctx, arg) + if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganizationMember.InOrg(arg.OrganizationID)); err != nil { + return nil, err + } + return q.db.PaginatedOrganizationMembers(ctx, arg) } func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 575744727ae60..ed82aa7625581 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -987,18 +987,12 @@ func (s *MethodTestSuite) TestOrganization() { })) s.Run("PaginatedOrganizationMembers", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) - u := dbgen.User(s.T(), db, database.User{}) - mem := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{ - OrganizationID: o.ID, - UserID: u.ID, - Roles: []string{rbac.RoleOrgAdmin()}, - }) check.Args(database.PaginatedOrganizationMembersParams{ - OrganizationID: uuid.UUID{}, + OrganizationID: o.ID, LimitOpt: 1, }).Asserts( - mem, policy.ActionRead, + rbac.ResourceOrganizationMember.InOrg(o.ID), policy.ActionRead, ) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { From 76a4cbf480b43babbfca9143a5b38f963de7fc16 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 7 Mar 2025 20:54:08 +0000 Subject: [PATCH 11/18] fix: incorrectly showing type in panic --- coderd/database/dbauthz/setup_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/setup_test.go b/coderd/database/dbauthz/setup_test.go index 4faac05b4746e..1a822254a9e7a 100644 --- a/coderd/database/dbauthz/setup_test.go +++ b/coderd/database/dbauthz/setup_test.go @@ -503,7 +503,7 @@ func asserts(inputs ...any) []AssertRBAC { // Could be the string type. actionAsString, ok := inputs[i+1].(string) if !ok { - panic(fmt.Sprintf("action '%q' not a supported action", actionAsString)) + panic(fmt.Sprintf("action '%T' not a supported action", inputs[i+1])) } action = policy.Action(actionAsString) } From 737ac9ff89e4ecfbfe238dd964b0e473749daa53 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 7 Mar 2025 21:56:48 +0000 Subject: [PATCH 12/18] fix: update docs --- coderd/apidoc/docs.go | 3 ++- coderd/apidoc/swagger.json | 3 ++- coderd/database/dbauthz/dbauthz_test.go | 16 +++++++++++++++- coderd/members.go | 3 ++- docs/reference/api/members.md | 10 +++++----- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 26ecfdbc70180..0fd3d1165ed8e 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2545,6 +2545,7 @@ const docTemplate = `{ ], "summary": "List organization members", "operationId": "list-organization-members", + "deprecated": true, "parameters": [ { "type": "string", @@ -2996,7 +2997,7 @@ const docTemplate = `{ }, { "type": "integer", - "description": "Page limit", + "description": "Page limit, if 0 returns all members", "name": "limit", "in": "query" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 8b9f538735c99..21546acb32ab3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2223,6 +2223,7 @@ "tags": ["Members"], "summary": "List organization members", "operationId": "list-organization-members", + "deprecated": true, "parameters": [ { "type": "string", @@ -2628,7 +2629,7 @@ }, { "type": "integer", - "description": "Page limit", + "description": "Page limit, if 0 returns all members", "name": "limit", "in": "query" }, diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ed82aa7625581..c12b519fc98ee 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -987,13 +987,27 @@ func (s *MethodTestSuite) TestOrganization() { })) s.Run("PaginatedOrganizationMembers", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) + u := dbgen.User(s.T(), db, database.User{}) + mem := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{ + OrganizationID: o.ID, + UserID: u.ID, + Roles: []string{rbac.RoleOrgAdmin()}, + }) check.Args(database.PaginatedOrganizationMembersParams{ OrganizationID: o.ID, LimitOpt: 1, }).Asserts( rbac.ResourceOrganizationMember.InOrg(o.ID), policy.ActionRead, - ) + ).Returns([]database.PaginatedOrganizationMembersRow{ + {OrganizationMember: mem, + Username: u.Username, + AvatarURL: u.AvatarURL, + Name: u.Name, + Email: u.Email, + GlobalRoles: u.RBACRoles, + Count: 1, + }}) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) diff --git a/coderd/members.go b/coderd/members.go index 0b2fd7b5757b8..1852e6448408f 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -142,6 +142,7 @@ func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request rw.WriteHeader(http.StatusNoContent) } +// @Deprecated use /organizations/{organization}/paginated-members [get] // @Summary List organization members // @ID list-organization-members // @Security CoderSessionToken @@ -184,7 +185,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Members // @Param organization path string true "Organization ID" -// @Param limit query int false "Page limit" +// @Param limit query int false "Page limit, if 0 returns all members" // @Param offset query int false "Page offset" // @Success 200 {object} []codersdk.PaginatedMembersResponse // @Router /organizations/{organization}/paginated-members [get] diff --git a/docs/reference/api/members.md b/docs/reference/api/members.md index 882323083a2ce..fd075f9f0d550 100644 --- a/docs/reference/api/members.md +++ b/docs/reference/api/members.md @@ -828,11 +828,11 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat ### Parameters -| Name | In | Type | Required | Description | -|----------------|-------|---------|----------|-----------------| -| `organization` | path | string | true | Organization ID | -| `limit` | query | integer | false | Page limit | -| `offset` | query | integer | false | Page offset | +| Name | In | Type | Required | Description | +|----------------|-------|---------|----------|--------------------------------------| +| `organization` | path | string | true | Organization ID | +| `limit` | query | integer | false | Page limit, if 0 returns all members | +| `offset` | query | integer | false | Page offset | ### Example responses From 8910df3465db7ebfbe1b397a83ca14ba0ca5b388 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 7 Mar 2025 21:59:25 +0000 Subject: [PATCH 13/18] fix: change dbauthz test back to proper one --- coderd/database/dbauthz/dbauthz_test.go | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index c12b519fc98ee..ed82aa7625581 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -987,27 +987,13 @@ func (s *MethodTestSuite) TestOrganization() { })) s.Run("PaginatedOrganizationMembers", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) - u := dbgen.User(s.T(), db, database.User{}) - mem := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{ - OrganizationID: o.ID, - UserID: u.ID, - Roles: []string{rbac.RoleOrgAdmin()}, - }) check.Args(database.PaginatedOrganizationMembersParams{ OrganizationID: o.ID, LimitOpt: 1, }).Asserts( rbac.ResourceOrganizationMember.InOrg(o.ID), policy.ActionRead, - ).Returns([]database.PaginatedOrganizationMembersRow{ - {OrganizationMember: mem, - Username: u.Username, - AvatarURL: u.AvatarURL, - Name: u.Name, - Email: u.Email, - GlobalRoles: u.RBACRoles, - Count: 1, - }}) + ) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) From 0e579a22b9fd5b78de2c4545fc941623fadbe21c Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Fri, 7 Mar 2025 22:05:42 +0000 Subject: [PATCH 14/18] fix: un-nest route --- coderd/coderd.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index ec8499ab86e60..da4e281dbe506 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1002,9 +1002,7 @@ func New(options *Options) *API { }) }) }) - r.Route("/paginated-members", func(r chi.Router) { - r.Get("/", api.paginatedMembers) - }) + r.Get("/paginated-members", api.paginatedMembers) r.Route("/members", func(r chi.Router) { r.Get("/", api.listMembers) r.Route("/roles", func(r chi.Router) { From 73e356c4fc889fac6e684294982fb2935d3b7b89 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Mon, 10 Mar 2025 16:00:56 +0000 Subject: [PATCH 15/18] fix: add comment --- coderd/database/dbauthz/dbauthz.go | 1 + coderd/database/dbauthz/dbauthz_test.go | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 19e10ca0f1f78..9c88e986cbffc 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -3582,6 +3582,7 @@ func (q *querier) OrganizationMembers(ctx context.Context, arg database.Organiza } func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database.PaginatedOrganizationMembersParams) ([]database.PaginatedOrganizationMembersRow, error) { + // Required to have permission to read all members in the organization if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceOrganizationMember.InOrg(arg.OrganizationID)); err != nil { return nil, err } diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index ed82aa7625581..7c57c3b717ab0 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -987,6 +987,12 @@ func (s *MethodTestSuite) TestOrganization() { })) s.Run("PaginatedOrganizationMembers", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) + u := dbgen.User(s.T(), db, database.User{}) + mem := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{ + OrganizationID: o.ID, + UserID: u.ID, + Roles: []string{rbac.RoleOrgAdmin()}, + }) check.Args(database.PaginatedOrganizationMembersParams{ OrganizationID: o.ID, From 24332f4c6804635d0ab784a047113d469db95a09 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Mon, 10 Mar 2025 16:01:58 +0000 Subject: [PATCH 16/18] fix: update test to account for dbmem and check for returned member --- coderd/database/dbauthz/dbauthz_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 7c57c3b717ab0..558b6d46a3b2d 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -996,10 +996,10 @@ func (s *MethodTestSuite) TestOrganization() { check.Args(database.PaginatedOrganizationMembersParams{ OrganizationID: o.ID, - LimitOpt: 1, + LimitOpt: 0, }).Asserts( rbac.ResourceOrganizationMember.InOrg(o.ID), policy.ActionRead, - ) + ).Returns(mem) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) From 7af0b606df885e5ed73f08f0b57257988468409a Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Mon, 10 Mar 2025 16:08:57 +0000 Subject: [PATCH 17/18] fix: update test to check for accurate count --- coderd/database/dbauthz/dbauthz_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 558b6d46a3b2d..ec8ced783fa0a 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -999,7 +999,17 @@ func (s *MethodTestSuite) TestOrganization() { LimitOpt: 0, }).Asserts( rbac.ResourceOrganizationMember.InOrg(o.ID), policy.ActionRead, - ).Returns(mem) + ).Returns([]database.PaginatedOrganizationMembersRow{ + { + OrganizationMember: mem, + Username: u.Username, + AvatarURL: u.AvatarURL, + Name: u.Name, + Email: u.Email, + GlobalRoles: u.RBACRoles, + Count: 1, + }, + }) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) From cdc03224adfc45813995705889c428855ad41fc5 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Mon, 10 Mar 2025 16:26:20 +0000 Subject: [PATCH 18/18] fix: make dbmem implementation match expected behaior around 0 limit --- coderd/database/dbmem/dbmem.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d0329e3c3444f..63ee1d0bd95e7 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -9593,34 +9593,42 @@ func (q *FakeQuerier) PaginatedOrganizationMembers(_ context.Context, arg databa q.mutex.RLock() defer q.mutex.RUnlock() - tmp := make([]database.PaginatedOrganizationMembersRow, 0) - - skippedMembers := 0 - for _, organizationMember := range q.organizationMembers { - if arg.OrganizationID != uuid.Nil && organizationMember.OrganizationID != arg.OrganizationID { + // All of the members in the organization + orgMembers := make([]database.OrganizationMember, 0) + for _, mem := range q.organizationMembers { + if arg.OrganizationID != uuid.Nil && mem.OrganizationID != arg.OrganizationID { continue } + orgMembers = append(orgMembers, mem) + } + + selectedMembers := make([]database.PaginatedOrganizationMembersRow, 0) + + skippedMembers := 0 + for _, organizationMember := range q.organizationMembers { if skippedMembers < int(arg.OffsetOpt) { skippedMembers++ continue } - if len(tmp) >= int(arg.LimitOpt) { + // if the limit is set to 0 we treat that as returning all of the org members + if int(arg.LimitOpt) != 0 && len(selectedMembers) >= int(arg.LimitOpt) { break } user, _ := q.getUserByIDNoLock(organizationMember.UserID) - tmp = append(tmp, database.PaginatedOrganizationMembersRow{ + selectedMembers = append(selectedMembers, database.PaginatedOrganizationMembersRow{ OrganizationMember: organizationMember, Username: user.Username, AvatarURL: user.AvatarURL, Name: user.Name, Email: user.Email, GlobalRoles: user.RBACRoles, + Count: int64(len(orgMembers)), }) } - return tmp, nil + return selectedMembers, nil } func (q *FakeQuerier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(_ context.Context, templateID uuid.UUID) error { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy