Skip to content

Commit cff829b

Browse files
committed
add trigger to delete role from organization members on delete
1 parent 6a140af commit cff829b

File tree

5 files changed

+142
-3
lines changed

5 files changed

+142
-3
lines changed

coderd/database/dbmem/dbmem.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,16 @@ func (q *FakeQuerier) DeleteCustomRole(_ context.Context, arg database.DeleteCus
13971397
if initial == len(q.data.customRoles) {
13981398
return sql.ErrNoRows
13991399
}
1400+
1401+
// Emulate the trigger 'remove_organization_member_custom_role'
1402+
for i, mem := range q.organizationMembers {
1403+
if mem.OrganizationID == arg.OrganizationID.UUID {
1404+
mem.Roles = slices.DeleteFunc(mem.Roles, func(role string) bool {
1405+
return role == arg.Name
1406+
})
1407+
q.organizationMembers[i] = mem
1408+
}
1409+
}
14001410
return nil
14011411
}
14021412

coderd/database/dump.sql

Lines changed: 22 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 remove_organization_member_custom_role ON custom_roles;
2+
DROP FUNCTION IF EXISTS remove_organization_member_role;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- When a custom role is deleted, we need to remove the assigned role
2+
-- from all organization members that have it.
3+
-- This action cannot be reverted, so deleting a custom role should be
4+
-- done with caution.
5+
CREATE OR REPLACE FUNCTION remove_organization_member_role()
6+
RETURNS TRIGGER AS
7+
$$
8+
BEGIN
9+
-- Delete the role from all organization members that have it.
10+
-- TODO: When site wide custom roles are supported, if the
11+
-- organization_id is null, we should remove the role from the 'users'
12+
-- table instead.
13+
IF OLD.organization_id IS NOT NULL THEN
14+
UPDATE organization_members
15+
-- this is a noop if the role is not assigned to the member
16+
SET roles = array_remove(roles, OLD.name)
17+
WHERE
18+
-- Scope to the correct organization
19+
organization_members.organization_id = OLD.organization_id;
20+
END IF;
21+
RETURN OLD;
22+
END;
23+
$$ LANGUAGE plpgsql;
24+
25+
26+
-- Attach the function to deleting the custom role
27+
CREATE TRIGGER remove_organization_member_custom_role
28+
BEFORE DELETE ON custom_roles FOR EACH ROW
29+
EXECUTE PROCEDURE remove_organization_member_role();

enterprise/coderd/roles_test.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,20 @@ func TestCustomOrganizationRole(t *testing.T) {
326326
},
327327
})
328328

329-
orgAdmin, _ := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
329+
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
330330
ctx := testutil.Context(t, testutil.WaitMedium)
331331

332-
//nolint:gocritic // owner is required for this
333-
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, first.OrganizationID, templateAdminCustom(first.OrganizationID))
332+
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
334333
require.NoError(t, err, "upsert role")
335334

335+
//nolint:gocritic // org_admin cannot assign to themselves
336+
_, err = owner.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, orgAdminUser.ID.String(), codersdk.UpdateRoles{
337+
// Give the user this custom role, to ensure when it is deleted, the user
338+
// is ok to be used.
339+
Roles: []string{createdRole.Name, rbac.ScopedRoleOrgAdmin(first.OrganizationID).Name},
340+
})
341+
require.NoError(t, err, "assign custom role to user")
342+
336343
existingRoles, err := orgAdmin.ListOrganizationRoles(ctx, first.OrganizationID)
337344
require.NoError(t, err)
338345

@@ -352,6 +359,75 @@ func TestCustomOrganizationRole(t *testing.T) {
352359
return role.Name == createdRole.Name
353360
})
354361
require.False(t, exists, "custom role should be deleted")
362+
363+
// Verify you can still assign roles.
364+
// There used to be a bug that if a member had a delete role, they
365+
// could not be assigned roles anymore.
366+
//nolint:gocritic // org_admin cannot assign to themselves
367+
_, err = owner.UpdateOrganizationMemberRoles(ctx, first.OrganizationID, orgAdminUser.ID.String(), codersdk.UpdateRoles{
368+
Roles: []string{rbac.ScopedRoleOrgAdmin(first.OrganizationID).Name},
369+
})
370+
require.NoError(t, err)
371+
})
372+
373+
// Verify deleting a custom role cascades to all members
374+
t.Run("DeleteRoleCascadeMembers", func(t *testing.T) {
375+
t.Parallel()
376+
dv := coderdtest.DeploymentValues(t)
377+
dv.Experiments = []string{string(codersdk.ExperimentCustomRoles)}
378+
owner, first := coderdenttest.New(t, &coderdenttest.Options{
379+
Options: &coderdtest.Options{
380+
DeploymentValues: dv,
381+
},
382+
LicenseOptions: &coderdenttest.LicenseOptions{
383+
Features: license.Features{
384+
codersdk.FeatureCustomRoles: 1,
385+
},
386+
},
387+
})
388+
389+
orgAdmin, orgAdminUser := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID))
390+
ctx := testutil.Context(t, testutil.WaitMedium)
391+
392+
createdRole, err := orgAdmin.PatchOrganizationRole(ctx, templateAdminCustom(first.OrganizationID))
393+
require.NoError(t, err, "upsert role")
394+
395+
customRoleIdentifier := rbac.RoleIdentifier{
396+
Name: createdRole.Name,
397+
OrganizationID: first.OrganizationID,
398+
}
399+
400+
// Create a few members with the role
401+
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, customRoleIdentifier)
402+
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID), customRoleIdentifier)
403+
coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(first.OrganizationID), rbac.ScopedRoleOrgAuditor(first.OrganizationID), customRoleIdentifier)
404+
405+
// Verify members have the custom role
406+
members, err := orgAdmin.OrganizationMembers(ctx, first.OrganizationID)
407+
require.NoError(t, err)
408+
require.Len(t, members, 5) // 3 members + org admin + owner
409+
for _, member := range members {
410+
if member.UserID == orgAdminUser.ID || member.UserID == first.UserID {
411+
continue
412+
}
413+
414+
require.True(t, slices.ContainsFunc(member.Roles, func(role codersdk.SlimRole) bool {
415+
return role.Name == customRoleIdentifier.Name
416+
}), "member should have custom role")
417+
}
418+
419+
err = orgAdmin.DeleteOrganizationRole(ctx, first.OrganizationID, createdRole.Name)
420+
require.NoError(t, err)
421+
422+
// Verify the role was removed from all members
423+
members, err = orgAdmin.OrganizationMembers(ctx, first.OrganizationID)
424+
require.NoError(t, err)
425+
require.Len(t, members, 5) // 3 members + org admin + owner
426+
for _, member := range members {
427+
require.False(t, slices.ContainsFunc(member.Roles, func(role codersdk.SlimRole) bool {
428+
return role.Name == customRoleIdentifier.Name
429+
}), "role should be removed from all users")
430+
}
355431
})
356432
}
357433

0 commit comments

Comments
 (0)