mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: implement deleting custom roles (#14101)
* chore: implement deleting custom roles * add trigger to delete role from organization members on delete * chore: add comments to explain populated field
This commit is contained in:
@@ -958,6 +958,20 @@ func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error {
|
||||
return q.db.DeleteCoordinator(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error {
|
||||
if arg.OrganizationID.UUID != uuid.Nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignRole); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return q.db.DeleteCustomRole(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
|
||||
return fetchAndExec(q.log, q.auth, policy.ActionUpdatePersonal, func(ctx context.Context, arg database.DeleteExternalAuthLinkParams) (database.ExternalAuthLink, error) {
|
||||
//nolint:gosimple
|
||||
|
||||
@@ -1247,6 +1247,31 @@ func (s *MethodTestSuite) TestUser() {
|
||||
s.Run("CustomRoles", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.CustomRolesParams{}).Asserts(rbac.ResourceAssignRole, policy.ActionRead).Returns([]database.CustomRole{})
|
||||
}))
|
||||
s.Run("Organization/DeleteCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: uuid.New(),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
check.Args(database.DeleteCustomRoleParams{
|
||||
Name: customRole.Name,
|
||||
OrganizationID: customRole.OrganizationID,
|
||||
}).Asserts(
|
||||
rbac.ResourceAssignOrgRole.InOrg(customRole.OrganizationID.UUID), policy.ActionDelete)
|
||||
}))
|
||||
s.Run("Site/DeleteCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
customRole := dbgen.CustomRole(s.T(), db, database.CustomRole{
|
||||
OrganizationID: uuid.NullUUID{
|
||||
UUID: uuid.Nil,
|
||||
Valid: false,
|
||||
},
|
||||
})
|
||||
check.Args(database.DeleteCustomRoleParams{
|
||||
Name: customRole.Name,
|
||||
}).Asserts(
|
||||
rbac.ResourceAssignRole, policy.ActionDelete)
|
||||
}))
|
||||
s.Run("Blank/UpsertCustomRole", s.Subtest(func(db database.Store, check *expects) {
|
||||
// Blank is no perms in the role
|
||||
check.Args(database.UpsertCustomRoleParams{
|
||||
|
||||
@@ -1381,6 +1381,35 @@ func (*FakeQuerier) DeleteCoordinator(context.Context, uuid.UUID) error {
|
||||
return ErrUnimplemented
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteCustomRole(_ context.Context, arg database.DeleteCustomRoleParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
initial := len(q.data.customRoles)
|
||||
q.data.customRoles = slices.DeleteFunc(q.data.customRoles, func(role database.CustomRole) bool {
|
||||
return role.OrganizationID.UUID == arg.OrganizationID.UUID && role.Name == arg.Name
|
||||
})
|
||||
if initial == len(q.data.customRoles) {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
// Emulate the trigger 'remove_organization_member_custom_role'
|
||||
for i, mem := range q.organizationMembers {
|
||||
if mem.OrganizationID == arg.OrganizationID.UUID {
|
||||
mem.Roles = slices.DeleteFunc(mem.Roles, func(role string) bool {
|
||||
return role == arg.Name
|
||||
})
|
||||
q.organizationMembers[i] = mem
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) DeleteExternalAuthLink(_ context.Context, arg database.DeleteExternalAuthLinkParams) error {
|
||||
err := validateDatabaseType(arg)
|
||||
if err != nil {
|
||||
|
||||
@@ -214,6 +214,13 @@ func (m metricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteCustomRole(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteCustomRole").Observe(time.Since(start).Seconds())
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m metricsStore) DeleteExternalAuthLink(ctx context.Context, arg database.DeleteExternalAuthLinkParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteExternalAuthLink(ctx, arg)
|
||||
|
||||
@@ -317,6 +317,20 @@ func (mr *MockStoreMockRecorder) DeleteCoordinator(arg0, arg1 any) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCoordinator", reflect.TypeOf((*MockStore)(nil).DeleteCoordinator), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteCustomRole mocks base method.
|
||||
func (m *MockStore) DeleteCustomRole(arg0 context.Context, arg1 database.DeleteCustomRoleParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteCustomRole", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteCustomRole indicates an expected call of DeleteCustomRole.
|
||||
func (mr *MockStoreMockRecorder) DeleteCustomRole(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCustomRole", reflect.TypeOf((*MockStore)(nil).DeleteCustomRole), arg0, arg1)
|
||||
}
|
||||
|
||||
// DeleteExternalAuthLink mocks base method.
|
||||
func (m *MockStore) DeleteExternalAuthLink(arg0 context.Context, arg1 database.DeleteExternalAuthLinkParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+24
@@ -302,6 +302,26 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION remove_organization_member_role() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Delete the role from all organization members that have it.
|
||||
-- TODO: When site wide custom roles are supported, if the
|
||||
-- organization_id is null, we should remove the role from the 'users'
|
||||
-- table instead.
|
||||
IF OLD.organization_id IS NOT NULL THEN
|
||||
UPDATE organization_members
|
||||
-- this is a noop if the role is not assigned to the member
|
||||
SET roles = array_remove(roles, OLD.name)
|
||||
WHERE
|
||||
-- Scope to the correct organization
|
||||
organization_members.organization_id = OLD.organization_id;
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION tailnet_notify_agent_change() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -1838,6 +1858,10 @@ CREATE UNIQUE INDEX workspaces_owner_id_lower_idx ON workspaces USING btree (own
|
||||
|
||||
CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled();
|
||||
|
||||
CREATE TRIGGER remove_organization_member_custom_role BEFORE DELETE ON custom_roles FOR EACH ROW EXECUTE FUNCTION remove_organization_member_role();
|
||||
|
||||
COMMENT ON TRIGGER remove_organization_member_custom_role ON custom_roles IS 'When a custom_role is deleted, this trigger removes the role from all organization members.';
|
||||
|
||||
CREATE TRIGGER tailnet_notify_agent_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_agents FOR EACH ROW EXECUTE FUNCTION tailnet_notify_agent_change();
|
||||
|
||||
CREATE TRIGGER tailnet_notify_client_change AFTER INSERT OR DELETE OR UPDATE ON tailnet_clients FOR EACH ROW EXECUTE FUNCTION tailnet_notify_client_change();
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TRIGGER IF EXISTS remove_organization_member_custom_role ON custom_roles;
|
||||
DROP FUNCTION IF EXISTS remove_organization_member_role;
|
||||
@@ -0,0 +1,35 @@
|
||||
-- When a custom role is deleted, we need to remove the assigned role
|
||||
-- from all organization members that have it.
|
||||
-- This action cannot be reverted, so deleting a custom role should be
|
||||
-- done with caution.
|
||||
CREATE OR REPLACE FUNCTION remove_organization_member_role()
|
||||
RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
-- Delete the role from all organization members that have it.
|
||||
-- TODO: When site wide custom roles are supported, if the
|
||||
-- organization_id is null, we should remove the role from the 'users'
|
||||
-- table instead.
|
||||
IF OLD.organization_id IS NOT NULL THEN
|
||||
UPDATE organization_members
|
||||
-- this is a noop if the role is not assigned to the member
|
||||
SET roles = array_remove(roles, OLD.name)
|
||||
WHERE
|
||||
-- Scope to the correct organization
|
||||
organization_members.organization_id = OLD.organization_id;
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
|
||||
-- Attach the function to deleting the custom role
|
||||
CREATE TRIGGER remove_organization_member_custom_role
|
||||
BEFORE DELETE ON custom_roles FOR EACH ROW
|
||||
EXECUTE PROCEDURE remove_organization_member_role();
|
||||
|
||||
|
||||
COMMENT ON TRIGGER
|
||||
remove_organization_member_custom_role
|
||||
ON custom_roles IS
|
||||
'When a custom_role is deleted, this trigger removes the role from all organization members.';
|
||||
@@ -69,6 +69,7 @@ type sqlcQuerier interface {
|
||||
DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error
|
||||
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteCoordinator(ctx context.Context, id uuid.UUID) error
|
||||
DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error
|
||||
DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error
|
||||
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteGroupByID(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
@@ -6520,6 +6520,24 @@ func (q *sqlQuerier) CustomRoles(ctx context.Context, arg CustomRolesParams) ([]
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const deleteCustomRole = `-- name: DeleteCustomRole :exec
|
||||
DELETE FROM
|
||||
custom_roles
|
||||
WHERE
|
||||
name = lower($1)
|
||||
AND organization_id = $2
|
||||
`
|
||||
|
||||
type DeleteCustomRoleParams struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteCustomRole, arg.Name, arg.OrganizationID)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertCustomRole = `-- name: UpsertCustomRole :one
|
||||
INSERT INTO
|
||||
custom_roles (
|
||||
|
||||
@@ -25,6 +25,13 @@ WHERE
|
||||
END
|
||||
;
|
||||
|
||||
-- name: DeleteCustomRole :exec
|
||||
DELETE FROM
|
||||
custom_roles
|
||||
WHERE
|
||||
name = lower(@name)
|
||||
AND organization_id = @organization_id
|
||||
;
|
||||
|
||||
-- name: UpsertCustomRole :one
|
||||
INSERT INTO
|
||||
|
||||
Reference in New Issue
Block a user