Skip to content

Commit 08e4131

Browse files
committed
chore: implement api layer for listing organization members
1 parent 2542dcd commit 08e4131

File tree

10 files changed

+353
-0
lines changed

10 files changed

+353
-0
lines changed

coderd/apidoc/docs.go

+67
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+63
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+1
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,7 @@ func New(options *Options) *API {
837837
})
838838
})
839839
r.Route("/members", func(r chi.Router) {
840+
r.Get("/", api.listMembers)
840841
r.Route("/roles", func(r chi.Router) {
841842
r.Get("/", api.assignableOrgRoles)
842843
r.With(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentCustomRoles)).

coderd/members.go

+41
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package coderd
33
import (
44
"net/http"
55

6+
"github.com/google/uuid"
7+
68
"github.com/coder/coder/v2/coderd/database/db2sdk"
79
"github.com/coder/coder/v2/coderd/rbac"
810

@@ -12,6 +14,36 @@ import (
1214
"github.com/coder/coder/v2/codersdk"
1315
)
1416

17+
// @Summary List organization members
18+
// @ID list-organization-members
19+
// @Security CoderSessionToken
20+
// @Produce json
21+
// @Tags Members
22+
// @Param organization path string true "Organization ID"
23+
// @Success 200 {object} []codersdk.OrganizationMemberWithName
24+
// @Router /organizations/{organization}/members [get]
25+
func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
26+
var (
27+
ctx = r.Context()
28+
organization = httpmw.OrganizationParam(r)
29+
)
30+
31+
members, err := api.Database.OrganizationMembers(ctx, database.OrganizationMembersParams{
32+
OrganizationID: organization.ID,
33+
UserID: uuid.Nil,
34+
})
35+
if httpapi.Is404Error(err) {
36+
httpapi.ResourceNotFound(rw)
37+
return
38+
}
39+
if err != nil {
40+
httpapi.InternalServerError(rw, err)
41+
return
42+
}
43+
44+
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.List(members, convertOrganizationMemberRow))
45+
}
46+
1547
// @Summary Assign role to organization member
1648
// @ID assign-role-to-organization-member
1749
// @Security CoderSessionToken
@@ -73,3 +105,12 @@ func convertOrganizationMember(mem database.OrganizationMember) codersdk.Organiz
73105
}
74106
return convertedMember
75107
}
108+
109+
func convertOrganizationMemberRow(row database.OrganizationMembersRow) codersdk.OrganizationMemberWithName {
110+
convertedMember := codersdk.OrganizationMemberWithName{
111+
Username: row.Username,
112+
OrganizationMember: convertOrganizationMember(row.OrganizationMember),
113+
}
114+
115+
return convertedMember
116+
}

coderd/members_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package coderd_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/uuid"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/coderd/coderdtest"
10+
"github.com/coder/coder/v2/coderd/database/db2sdk"
11+
"github.com/coder/coder/v2/coderd/rbac"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/coder/v2/testutil"
14+
)
15+
16+
func TestListMembers(t *testing.T) {
17+
t.Parallel()
18+
19+
t.Run("OK", func(t *testing.T) {
20+
t.Parallel()
21+
owner := coderdtest.New(t, nil)
22+
first := coderdtest.CreateFirstUser(t, owner)
23+
24+
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
25+
26+
ctx := testutil.Context(t, testutil.WaitShort)
27+
members, err := client.OrganizationMembers(ctx, first.OrganizationID)
28+
require.NoError(t, err)
29+
require.Len(t, members, 2)
30+
require.ElementsMatch(t,
31+
[]uuid.UUID{first.UserID, user.ID},
32+
db2sdk.List(members, onlyIDs))
33+
})
34+
35+
// Calling it from a user without the org access.
36+
t.Run("NotInOrg", func(t *testing.T) {
37+
t.Parallel()
38+
owner := coderdtest.New(t, nil)
39+
first := coderdtest.CreateFirstUser(t, owner)
40+
41+
client, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
42+
43+
ctx := testutil.Context(t, testutil.WaitShort)
44+
org, err := owner.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
45+
Name: "test",
46+
DisplayName: "",
47+
Description: "",
48+
})
49+
require.NoError(t, err, "create organization")
50+
51+
// 404 error is expected instead of a 403/401 to not leak existence of
52+
// an organization.
53+
_, err = client.OrganizationMembers(ctx, org.ID)
54+
require.ErrorContains(t, err, "404")
55+
})
56+
}
57+
58+
func onlyIDs(u codersdk.OrganizationMemberWithName) uuid.UUID {
59+
return u.UserID
60+
}

codersdk/organizations.go

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ type OrganizationMember struct {
5757
Roles []SlimRole `db:"roles" json:"roles"`
5858
}
5959

60+
type OrganizationMemberWithName struct {
61+
Username string `table:"username,default_sort" json:"username"`
62+
OrganizationMember `table:"m,recursive_inline"`
63+
}
64+
6065
type CreateOrganizationRequest struct {
6166
Name string `json:"name" validate:"required,organization_name"`
6267
// DisplayName will default to the same value as `Name` if not provided.

codersdk/users.go

+14
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,20 @@ func (c *Client) UpdateUserPassword(ctx context.Context, user string, req Update
379379
return nil
380380
}
381381

382+
// OrganizationMembers lists all members in an organization
383+
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]OrganizationMemberWithName, error) {
384+
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil)
385+
if err != nil {
386+
return nil, err
387+
}
388+
defer res.Body.Close()
389+
if res.StatusCode != http.StatusOK {
390+
return nil, ReadBodyAsError(res)
391+
}
392+
var members []OrganizationMemberWithName
393+
return members, json.NewDecoder(res.Body).Decode(&members)
394+
}
395+
382396
// UpdateUserRoles grants the userID the specified roles.
383397
// Include ALL roles the user has.
384398
func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) {

docs/api/members.md

+67
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)