Make organization deletion cascade through credentials and immutable audit logs

This commit is contained in:
ZacharyZhang-NY committed 2026-05-30 00:13:24 -04:00
1 parent e14a4bfce8
commit c6e29d7fbe
5 files changed
+154 -1

No files matched your search

@@ -0,0 +1,46 @@
// Deleting an organization cascades into its `credentials`, but
// `provider_auth_profiles.credential_id` referenced `credentials(id)` with
// `on delete restrict`, which blocked the cascade and made the whole DELETE
// throw — so deleting an organization silently failed. This rebuilds the
// (leaf) table with `on delete cascade`, preserving every existing row and
// index. Revocation only deactivates a profile (active = 0) and never deletes
// a credential row, so cascade has no effect on normal operation.
export const providerAuthProfileCascadeSchema = `
create table provider_auth_profiles_rebuild (
id text primary key,
org_id text not null references organizations(id) on delete cascade,
provider_id text not null references providers(id) on delete cascade,
credential_id text not null references credentials(id) on delete cascade,
provider_key text not null,
auth_type text not null,
profile_kind text not null,
display_hint text not null,
source_type text not null,
source_ref text,
capability_json text not null,
active integer not null check(active in (0, 1)),
created_at text not null
);
insert into provider_auth_profiles_rebuild(
id, org_id, provider_id, credential_id, provider_key, auth_type,
profile_kind, display_hint, source_type, source_ref, capability_json, active, created_at
)
select
id, org_id, provider_id, credential_id, provider_key, auth_type,
profile_kind, display_hint, source_type, source_ref, capability_json, active, created_at
from provider_auth_profiles;
drop table provider_auth_profiles;
alter table provider_auth_profiles_rebuild rename to provider_auth_profiles;
create index if not exists idx_provider_auth_profiles_org_provider
on provider_auth_profiles(org_id, provider_id, created_at);
create index if not exists idx_provider_auth_profiles_credential
on provider_auth_profiles(credential_id);
create unique index if not exists idx_provider_auth_profiles_active
on provider_auth_profiles(provider_id) where active = 1;
insert into schema_migrations(version, name, applied_at)
values (38, 'provider_auth_profile_cascade', datetime('now'));
`;
@@ -0,0 +1,28 @@
// The audit_events and social_audit_records immutability triggers raised ABORT
// on EVERY delete, including the legitimate cascade when an organization is torn
// down — so deleting an organization always rolled back. These rebuilt triggers
// still block tampering deletes of individual records (the owning org exists),
// but allow the cascade once the organization row itself is gone. Direct
// deletes of audit rows whose org still exists remain forbidden.
export const auditImmutabilityOrgTeardownSchema = `
drop trigger if exists trg_audit_events_no_delete;
create trigger trg_audit_events_no_delete
before delete on audit_events
for each row
when exists (select 1 from organizations where id = old.org_id)
begin
select raise(abort, 'Audit events are immutable.');
end;
drop trigger if exists trg_social_audit_records_no_delete;
create trigger trg_social_audit_records_no_delete
before delete on social_audit_records
for each row
when exists (select 1 from organizations where id = old.org_id)
begin
select raise(abort, 'Social audit records are immutable.');
end;
insert into schema_migrations(version, name, applied_at)
values (39, 'audit_immutability_org_teardown', datetime('now'));
`;
@@ -36,6 +36,8 @@ import { providerAuthSessionsSchema } from "./migrations/034-provider-auth-sessi
import { taskRunIsolationSchema } from "./migrations/035-task-run-isolation";
import { additionalConnectorsSchema } from "./migrations/036-additional-connectors";
import { customProvidersSchema } from "./migrations/037-custom-providers";
import { providerAuthProfileCascadeSchema } from "./migrations/038-provider-auth-profile-cascade";
import { auditImmutabilityOrgTeardownSchema } from "./migrations/039-audit-immutability-org-teardown";
interface Migration {
version: number;
@@ -81,6 +83,8 @@ const MIGRATIONS: Migration[] = [
{ version: 35, name: "task_run_isolation", sql: taskRunIsolationSchema },
{ version: 36, name: "additional_connectors", sql: additionalConnectorsSchema },
{ version: 37, name: "custom_providers", sql: customProvidersSchema },
{ version: 38, name: "provider_auth_profile_cascade", sql: providerAuthProfileCascadeSchema },
{ version: 39, name: "audit_immutability_org_teardown", sql: auditImmutabilityOrgTeardownSchema },
];
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;