Skip to content

Commit 368790f

Browse files
committed
chore: implement delete organization member
Side effects of removing an organization member will orphan their user resources. These side effects are not addressed here
1 parent 909a7ce commit 368790f

File tree

15 files changed

+295
-0
lines changed

15 files changed

+295
-0
lines changed

coderd/apidoc/docs.go

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,7 @@ func New(options *Options) *API {
860860
r.Use(
861861
httpmw.ExtractOrganizationMemberParam(options.Database),
862862
)
863+
r.Delete("/", api.deleteOrganizationMember)
863864
r.Put("/roles", api.putMemberRoles)
864865
r.Post("/workspaces", api.postWorkspacesByOrganization)
865866
})

coderd/database/dbauthz/dbauthz.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,16 @@ func (q *querier) DeleteOrganization(ctx context.Context, id uuid.UUID) error {
10351035
return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, q.db.DeleteOrganization)(ctx, id)
10361036
}
10371037

1038+
func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
1039+
return deleteQ[database.OrganizationMember](q.log, q.auth, func(ctx context.Context, arg database.DeleteOrganizationMemberParams) (database.OrganizationMember, error) {
1040+
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams(arg)))
1041+
if err != nil {
1042+
return database.OrganizationMember{}, err
1043+
}
1044+
return member.OrganizationMember, nil
1045+
}, q.db.DeleteOrganizationMember)(ctx, arg)
1046+
}
1047+
10381048
func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error {
10391049
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
10401050
return err

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,23 @@ func (s *MethodTestSuite) TestOrganization() {
628628
rbac.ResourceAssignOrgRole.InOrg(o.ID), policy.ActionAssign,
629629
rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate)
630630
}))
631+
s.Run("DeleteOrganizationMember", s.Subtest(func(db database.Store, check *expects) {
632+
o := dbgen.Organization(s.T(), db, database.Organization{})
633+
u := dbgen.User(s.T(), db, database.User{})
634+
member := dbgen.OrganizationMember(s.T(), db, database.OrganizationMember{UserID: u.ID, OrganizationID: o.ID})
635+
636+
check.Args(database.DeleteOrganizationMemberParams{
637+
OrganizationID: o.ID,
638+
UserID: u.ID,
639+
}).Asserts(
640+
// Reads the org member before it tries to delete it
641+
member, policy.ActionRead,
642+
member, policy.ActionDelete).
643+
// SQL Filter returns a 404
644+
WithNotAuthorized("no rows").
645+
WithCancelled("no rows").
646+
Errors(sql.ErrNoRows)
647+
}))
631648
s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) {
632649
o := dbgen.Organization(s.T(), db, database.Organization{
633650
Name: "something-unique",

coderd/database/dbmem/dbmem.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,24 @@ func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error
16321632
return sql.ErrNoRows
16331633
}
16341634

1635+
func (q *FakeQuerier) DeleteOrganizationMember(_ context.Context, arg database.DeleteOrganizationMemberParams) error {
1636+
err := validateDatabaseType(arg)
1637+
if err != nil {
1638+
return err
1639+
}
1640+
1641+
q.mutex.Lock()
1642+
defer q.mutex.Unlock()
1643+
1644+
deleted := slices.DeleteFunc(q.data.organizationMembers, func(member database.OrganizationMember) bool {
1645+
return member.OrganizationID == arg.OrganizationID && member.UserID == arg.UserID
1646+
})
1647+
if len(deleted) == 0 {
1648+
return sql.ErrNoRows
1649+
}
1650+
return nil
1651+
}
1652+
16351653
func (q *FakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error {
16361654
q.mutex.Lock()
16371655
defer q.mutex.Unlock()

coderd/database/dbmetrics/dbmetrics.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbmock/dbmock.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/organizationmembers.sql

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ INSERT INTO
3636
VALUES
3737
($1, $2, $3, $4, $5) RETURNING *;
3838

39+
-- name: DeleteOrganizationMember :exec
40+
DELETE
41+
FROM
42+
organization_members
43+
WHERE
44+
organization_id = @organization_id AND
45+
user_id = @user_id
46+
;
47+
3948

4049
-- name: GetOrganizationIDsByMemberIDs :many
4150
SELECT

coderd/members.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,38 @@ func (api *API) postOrganizationMember(rw http.ResponseWriter, r *http.Request)
6262
httpapi.Write(ctx, rw, http.StatusOK, resp[0])
6363
}
6464

65+
// @Summary Remove organization member
66+
// @ID remove-organization-member
67+
// @Security CoderSessionToken
68+
// @Produce json
69+
// @Tags Members
70+
// @Param organization path string true "Organization ID"
71+
// @Param user path string true "User ID, name, or me"
72+
// @Success 200 {object} codersdk.OrganizationMember
73+
// @Router /organizations/{organization}/members/{user} [delete]
74+
func (api *API) deleteOrganizationMember(rw http.ResponseWriter, r *http.Request) {
75+
var (
76+
ctx = r.Context()
77+
organization = httpmw.OrganizationParam(r)
78+
member = httpmw.OrganizationMemberParam(r)
79+
)
80+
81+
err := api.Database.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{
82+
OrganizationID: organization.ID,
83+
UserID: member.UserID,
84+
})
85+
if httpapi.Is404Error(err) {
86+
httpapi.ResourceNotFound(rw)
87+
return
88+
}
89+
if err != nil {
90+
httpapi.InternalServerError(rw, err)
91+
return
92+
}
93+
94+
httpapi.Write(ctx, rw, http.StatusOK, "organization member removed")
95+
}
96+
6597
// @Summary List organization members
6698
// @ID list-organization-members
6799
// @Security CoderSessionToken

coderd/members_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,38 @@ func TestListMembers(t *testing.T) {
9292
})
9393
}
9494

95+
func TestRemoveMember(t *testing.T) {
96+
t.Parallel()
97+
98+
t.Run("OK", func(t *testing.T) {
99+
t.Parallel()
100+
owner := coderdtest.New(t, nil)
101+
first := coderdtest.CreateFirstUser(t, owner)
102+
orgAdminClient, orgAdmin := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
103+
_, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)
104+
105+
ctx := testutil.Context(t, testutil.WaitMedium)
106+
// Verify the org of 3 members
107+
members, err := orgAdminClient.OrganizationMembers(ctx, first.OrganizationID)
108+
require.NoError(t, err)
109+
require.Len(t, members, 3)
110+
require.ElementsMatch(t,
111+
[]uuid.UUID{first.UserID, user.ID, orgAdmin.ID},
112+
db2sdk.List(members, onlyIDs))
113+
114+
// Delete a member
115+
err = orgAdminClient.DeleteOrganizationMember(ctx, first.OrganizationID, user.Username)
116+
require.NoError(t, err)
117+
118+
members, err = orgAdminClient.OrganizationMembers(ctx, first.OrganizationID)
119+
require.NoError(t, err)
120+
require.Len(t, members, 2)
121+
require.ElementsMatch(t,
122+
[]uuid.UUID{first.UserID, orgAdmin.ID},
123+
db2sdk.List(members, onlyIDs))
124+
})
125+
}
126+
95127
func onlyIDs(u codersdk.OrganizationMemberWithName) uuid.UUID {
96128
return u.UserID
97129
}

codersdk/users.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,19 @@ func (c *Client) PostOrganizationMember(ctx context.Context, organizationID uuid
393393
return member, json.NewDecoder(res.Body).Decode(&member)
394394
}
395395

396+
// DeleteOrganizationMember removes a user from an organization
397+
func (c *Client) DeleteOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) error {
398+
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
399+
if err != nil {
400+
return err
401+
}
402+
defer res.Body.Close()
403+
if res.StatusCode != http.StatusOK {
404+
return ReadBodyAsError(res)
405+
}
406+
return nil
407+
}
408+
396409
// OrganizationMembers lists all members in an organization
397410
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]OrganizationMemberWithName, error) {
398411
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil)

docs/api/members.md

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)