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:
Steven Masley
2024-08-07 12:37:55 -05:00
committed by GitHub
parent d0feb70811
commit 2c13797350
19 changed files with 627 additions and 2 deletions
+14
View File
@@ -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
+25
View File
@@ -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{
+29
View File
@@ -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 {
+7
View File
@@ -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)
+14
View File
@@ -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()
+24
View File
@@ -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.';
+1
View File
@@ -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
+18
View File
@@ -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 (
+7
View File
@@ -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