feat: enable soft delete for organizations (#16584)

- Add deleted column to organizations table
- Add trigger to check for existing workspaces, templates, groups and
members in a org before allowing the soft delete

---------

Co-authored-by: Steven Masley <stevenmasley@gmail.com>
Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
This commit is contained in:
Jaayden Halko
2025-02-24 17:59:41 +00:00
committed by GitHub
parent dfa33b11d9
commit 546a549dcf
28 changed files with 605 additions and 215 deletions
+73 -7
View File
@@ -438,6 +438,74 @@ BEGIN
END;
$$;
CREATE FUNCTION protect_deleting_organizations() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
workspace_count int;
template_count int;
group_count int;
member_count int;
provisioner_keys_count int;
BEGIN
workspace_count := (
SELECT count(*) as count FROM workspaces
WHERE
workspaces.organization_id = OLD.id
AND workspaces.deleted = false
);
template_count := (
SELECT count(*) as count FROM templates
WHERE
templates.organization_id = OLD.id
AND templates.deleted = false
);
group_count := (
SELECT count(*) as count FROM groups
WHERE
groups.organization_id = OLD.id
);
member_count := (
SELECT count(*) as count FROM organization_members
WHERE
organization_members.organization_id = OLD.id
);
provisioner_keys_count := (
Select count(*) as count FROM provisioner_keys
WHERE
provisioner_keys.organization_id = OLD.id
);
-- Fail the deletion if one of the following:
-- * the organization has 1 or more workspaces
-- * the organization has 1 or more templates
-- * the organization has 1 or more groups other than "Everyone" group
-- * the organization has 1 or more members other than the organization owner
-- * the organization has 1 or more provisioner keys
IF (workspace_count + template_count + provisioner_keys_count) > 0 THEN
RAISE EXCEPTION 'cannot delete organization: organization has % workspaces, % templates, and % provisioner keys that must be deleted first', workspace_count, template_count, provisioner_keys_count;
END IF;
IF (group_count) > 1 THEN
RAISE EXCEPTION 'cannot delete organization: organization has % groups that must be deleted first', group_count - 1;
END IF;
-- Allow 1 member to exist, because you cannot remove yourself. You can
-- remove everyone else. Ideally, we only omit the member that matches
-- the user_id of the caller, however in a trigger, the caller is unknown.
IF (member_count) > 1 THEN
RAISE EXCEPTION 'cannot delete organization: organization has % members that must be deleted first', member_count - 1;
END IF;
RETURN NEW;
END;
$$;
CREATE FUNCTION provisioner_tagset_contains(provisioner_tags tagset, job_tags tagset) RETURNS boolean
LANGUAGE plpgsql
AS $$
@@ -967,7 +1035,8 @@ CREATE TABLE organizations (
updated_at timestamp with time zone NOT NULL,
is_default boolean DEFAULT false NOT NULL,
display_name text NOT NULL,
icon text DEFAULT ''::text NOT NULL
icon text DEFAULT ''::text NOT NULL,
deleted boolean DEFAULT false NOT NULL
);
CREATE TABLE parameter_schemas (
@@ -2030,9 +2099,6 @@ ALTER TABLE ONLY oauth2_provider_apps
ALTER TABLE ONLY organization_members
ADD CONSTRAINT organization_members_pkey PRIMARY KEY (organization_id, user_id);
ALTER TABLE ONLY organizations
ADD CONSTRAINT organizations_name UNIQUE (name);
ALTER TABLE ONLY organizations
ADD CONSTRAINT organizations_pkey PRIMARY KEY (id);
@@ -2218,9 +2284,7 @@ CREATE INDEX idx_organization_member_organization_id_uuid ON organization_member
CREATE INDEX idx_organization_member_user_id_uuid ON organization_members USING btree (user_id);
CREATE UNIQUE INDEX idx_organization_name ON organizations USING btree (name);
CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name));
CREATE UNIQUE INDEX idx_organization_name_lower ON organizations USING btree (lower(name)) WHERE (deleted = false);
CREATE UNIQUE INDEX idx_provisioner_daemons_org_name_owner_key ON provisioner_daemons USING btree (organization_id, name, lower(COALESCE((tags ->> 'owner'::text), ''::text)));
@@ -2352,6 +2416,8 @@ CREATE OR REPLACE VIEW provisioner_job_stats AS
CREATE TRIGGER inhibit_enqueue_if_disabled BEFORE INSERT ON notification_messages FOR EACH ROW EXECUTE FUNCTION inhibit_enqueue_if_disabled();
CREATE TRIGGER protect_deleting_organizations BEFORE UPDATE ON organizations FOR EACH ROW WHEN (((new.deleted = true) AND (old.deleted = false))) EXECUTE FUNCTION protect_deleting_organizations();
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.';