Skip to content

Commit 1e5438e

Browse files
authored
feat: remove user from groups on org membership delete (#14701)
* feat: remove user from groups on org membership delete Groups inherently provide authz access to certain resources. If a user is removed from an organization, they should be removed from all their groups in said organization.
1 parent c145f11 commit 1e5438e

File tree

6 files changed

+196
-1
lines changed

6 files changed

+196
-1
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,7 +1944,7 @@ func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error
19441944
return sql.ErrNoRows
19451945
}
19461946

1947-
func (q *FakeQuerier) DeleteOrganizationMember(_ context.Context, arg database.DeleteOrganizationMemberParams) error {
1947+
func (q *FakeQuerier) DeleteOrganizationMember(ctx context.Context, arg database.DeleteOrganizationMemberParams) error {
19481948
err := validateDatabaseType(arg)
19491949
if err != nil {
19501950
return err
@@ -1959,6 +1959,16 @@ func (q *FakeQuerier) DeleteOrganizationMember(_ context.Context, arg database.D
19591959
if len(deleted) == 0 {
19601960
return sql.ErrNoRows
19611961
}
1962+
1963+
// Delete group member trigger
1964+
q.groupMembers = slices.DeleteFunc(q.groupMembers, func(member database.GroupMemberTable) bool {
1965+
if member.UserID != arg.UserID {
1966+
return false
1967+
}
1968+
g, _ := q.getGroupByIDNoLock(ctx, member.GroupID)
1969+
return g.OrganizationID == arg.OrganizationID
1970+
})
1971+
19621972
return nil
19631973
}
19641974

coderd/database/dump.sql

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TRIGGER IF EXISTS trigger_delete_group_members_on_org_member_delete ON organization_members;
2+
DROP FUNCTION IF EXISTS delete_group_members_on_org_member_delete;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE FUNCTION delete_group_members_on_org_member_delete() RETURNS TRIGGER
2+
LANGUAGE plpgsql
3+
AS $$
4+
DECLARE
5+
BEGIN
6+
-- Remove the user from all groups associated with the same
7+
-- organization as the organization_member being deleted.
8+
DELETE FROM group_members
9+
WHERE
10+
user_id = OLD.user_id
11+
AND group_id IN (
12+
SELECT id
13+
FROM groups
14+
WHERE organization_id = OLD.organization_id
15+
);
16+
RETURN OLD;
17+
END;
18+
$$;
19+
20+
CREATE TRIGGER trigger_delete_group_members_on_org_member_delete
21+
BEFORE DELETE ON organization_members
22+
FOR EACH ROW
23+
EXECUTE PROCEDURE delete_group_members_on_org_member_delete();

coderd/database/querier_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,103 @@ func TestExpectOne(t *testing.T) {
12161216
})
12171217
}
12181218

1219+
func TestGroupRemovalTrigger(t *testing.T) {
1220+
t.Parallel()
1221+
1222+
db, _ := dbtestutil.NewDB(t)
1223+
1224+
orgA := dbgen.Organization(t, db, database.Organization{})
1225+
_, err := db.InsertAllUsersGroup(context.Background(), orgA.ID)
1226+
require.NoError(t, err)
1227+
1228+
orgB := dbgen.Organization(t, db, database.Organization{})
1229+
_, err = db.InsertAllUsersGroup(context.Background(), orgB.ID)
1230+
require.NoError(t, err)
1231+
1232+
orgs := []database.Organization{orgA, orgB}
1233+
1234+
user := dbgen.User(t, db, database.User{})
1235+
extra := dbgen.User(t, db, database.User{})
1236+
users := []database.User{user, extra}
1237+
1238+
groupA1 := dbgen.Group(t, db, database.Group{
1239+
OrganizationID: orgA.ID,
1240+
})
1241+
groupA2 := dbgen.Group(t, db, database.Group{
1242+
OrganizationID: orgA.ID,
1243+
})
1244+
1245+
groupB1 := dbgen.Group(t, db, database.Group{
1246+
OrganizationID: orgB.ID,
1247+
})
1248+
groupB2 := dbgen.Group(t, db, database.Group{
1249+
OrganizationID: orgB.ID,
1250+
})
1251+
1252+
groups := []database.Group{groupA1, groupA2, groupB1, groupB2}
1253+
1254+
// Add users to all organizations
1255+
for _, u := range users {
1256+
for _, o := range orgs {
1257+
dbgen.OrganizationMember(t, db, database.OrganizationMember{
1258+
OrganizationID: o.ID,
1259+
UserID: u.ID,
1260+
})
1261+
}
1262+
}
1263+
1264+
// Add users to all groups
1265+
for _, u := range users {
1266+
for _, g := range groups {
1267+
dbgen.GroupMember(t, db, database.GroupMemberTable{
1268+
GroupID: g.ID,
1269+
UserID: u.ID,
1270+
})
1271+
}
1272+
}
1273+
1274+
// Verify user is in all groups
1275+
ctx := testutil.Context(t, testutil.WaitLong)
1276+
onlyGroupIDs := func(row database.GetGroupsRow) uuid.UUID {
1277+
return row.Group.ID
1278+
}
1279+
userGroups, err := db.GetGroups(ctx, database.GetGroupsParams{
1280+
HasMemberID: user.ID,
1281+
})
1282+
require.NoError(t, err)
1283+
require.ElementsMatch(t, []uuid.UUID{
1284+
orgA.ID, orgB.ID, // Everyone groups
1285+
groupA1.ID, groupA2.ID, groupB1.ID, groupB2.ID, // Org groups
1286+
}, db2sdk.List(userGroups, onlyGroupIDs))
1287+
1288+
// Remove the user from org A
1289+
err = db.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{
1290+
OrganizationID: orgA.ID,
1291+
UserID: user.ID,
1292+
})
1293+
require.NoError(t, err)
1294+
1295+
// Verify user is no longer in org A groups
1296+
userGroups, err = db.GetGroups(ctx, database.GetGroupsParams{
1297+
HasMemberID: user.ID,
1298+
})
1299+
require.NoError(t, err)
1300+
require.ElementsMatch(t, []uuid.UUID{
1301+
orgB.ID, // Everyone group
1302+
groupB1.ID, groupB2.ID, // Org groups
1303+
}, db2sdk.List(userGroups, onlyGroupIDs))
1304+
1305+
// Verify extra user is unchanged
1306+
extraUserGroups, err := db.GetGroups(ctx, database.GetGroupsParams{
1307+
HasMemberID: extra.ID,
1308+
})
1309+
require.NoError(t, err)
1310+
require.ElementsMatch(t, []uuid.UUID{
1311+
orgA.ID, orgB.ID, // Everyone groups
1312+
groupA1.ID, groupA2.ID, groupB1.ID, groupB2.ID, // Org groups
1313+
}, db2sdk.List(extraUserGroups, onlyGroupIDs))
1314+
}
1315+
12191316
func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) {
12201317
t.Helper()
12211318
require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg)

enterprise/members_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestEnterpriseMembers(t *testing.T) {
2929
LicenseOptions: &coderdenttest.LicenseOptions{
3030
Features: license.Features{
3131
codersdk.FeatureMultipleOrganizations: 1,
32+
codersdk.FeatureTemplateRBAC: 1,
3233
},
3334
},
3435
})
@@ -39,6 +40,21 @@ func TestEnterpriseMembers(t *testing.T) {
3940
_, user := coderdtest.CreateAnotherUser(t, owner, secondOrg.ID)
4041

4142
ctx := testutil.Context(t, testutil.WaitMedium)
43+
44+
// Groups exist to ensure a user removed from the org loses their
45+
// group access.
46+
g1, err := orgAdminClient.CreateGroup(ctx, secondOrg.ID, codersdk.CreateGroupRequest{
47+
Name: "foo",
48+
DisplayName: "Foo",
49+
})
50+
require.NoError(t, err)
51+
52+
g2, err := orgAdminClient.CreateGroup(ctx, secondOrg.ID, codersdk.CreateGroupRequest{
53+
Name: "bar",
54+
DisplayName: "Bar",
55+
})
56+
require.NoError(t, err)
57+
4258
// Verify the org of 3 members
4359
members, err := orgAdminClient.OrganizationMembers(ctx, secondOrg.ID)
4460
require.NoError(t, err)
@@ -47,6 +63,25 @@ func TestEnterpriseMembers(t *testing.T) {
4763
[]uuid.UUID{first.UserID, user.ID, orgAdmin.ID},
4864
db2sdk.List(members, onlyIDs))
4965

66+
// Add the member to some groups
67+
_, err = orgAdminClient.PatchGroup(ctx, g1.ID, codersdk.PatchGroupRequest{
68+
AddUsers: []string{user.ID.String()},
69+
})
70+
require.NoError(t, err)
71+
72+
_, err = orgAdminClient.PatchGroup(ctx, g2.ID, codersdk.PatchGroupRequest{
73+
AddUsers: []string{user.ID.String()},
74+
})
75+
require.NoError(t, err)
76+
77+
// Verify group membership
78+
userGroups, err := orgAdminClient.Groups(ctx, codersdk.GroupArguments{
79+
HasMember: user.ID.String(),
80+
})
81+
require.NoError(t, err)
82+
// Everyone group + 2 groups
83+
require.Len(t, userGroups, 3)
84+
5085
// Delete a member
5186
err = orgAdminClient.DeleteOrganizationMember(ctx, secondOrg.ID, user.Username)
5287
require.NoError(t, err)
@@ -57,6 +92,13 @@ func TestEnterpriseMembers(t *testing.T) {
5792
require.ElementsMatch(t,
5893
[]uuid.UUID{first.UserID, orgAdmin.ID},
5994
db2sdk.List(members, onlyIDs))
95+
96+
// User should now belong to 0 groups
97+
userGroups, err = orgAdminClient.Groups(ctx, codersdk.GroupArguments{
98+
HasMember: user.ID.String(),
99+
})
100+
require.NoError(t, err)
101+
require.Len(t, userGroups, 0)
60102
})
61103

62104
t.Run("PostUser", func(t *testing.T) {

0 commit comments

Comments
 (0)
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