Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions supabase/migrations/008_procore_audit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,21 @@ create index if not exists idx_procore_audit_company_created
create index if not exists idx_procore_audit_created
on private.procore_audit (created_at);

-- Append-only: block UPDATE always; block DELETE for non-admin/owner roles.
-- The retention/deletion functions below are SECURITY DEFINER and run as the
-- function owner, so they pass the DELETE guard while ad-hoc deletes do not.
-- Append-only: block UPDATE always; block DELETE unless a controlled purge
-- function has enabled the transaction-local delete guard.
-- The retention/deletion functions below set a transaction-local guard before
-- deleting, so controlled purges do not depend on the migration owner's role.
create or replace function private.prevent_procore_audit_mutation()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
if tg_op = 'UPDATE' then
raise exception 'private.procore_audit is append-only; UPDATE not permitted.';
end if;
if current_user not in ('audit_admin', 'postgres') then
if coalesce(current_setting('app.procore_audit_delete_allowed', true), '') <> 'on' then
raise exception 'private.procore_audit is append-only; DELETE not permitted.';
end if;
return old;
Expand Down Expand Up @@ -129,10 +131,16 @@ as $$
declare
v_deleted integer;
begin
perform set_config('app.procore_audit_delete_allowed', 'on', true);
delete from private.procore_audit
where created_at < now() - (retention_days || ' days')::interval;
perform set_config('app.procore_audit_delete_allowed', 'off', true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture row count before clearing the guard

When this purge deletes any number of audit rows, the new perform set_config(..., 'off', true) becomes the most recent SQL statement before GET DIAGNOSTICS v_deleted = row_count, so the function reports the row count from set_config instead of the DELETE. This makes the RPC return an incorrect deletion count (and the same ordering appears in delete_procore_audit_for_company below); store row_count immediately after the DELETE, then clear the guard.

Useful? React with 👍 / 👎.

get diagnostics v_deleted = row_count;
return v_deleted;
exception
when others then
perform set_config('app.procore_audit_delete_allowed', 'off', true);
raise;
end;
$$;

Expand All @@ -146,9 +154,15 @@ as $$
declare
v_deleted integer;
begin
perform set_config('app.procore_audit_delete_allowed', 'on', true);
delete from private.procore_audit where company_id = p_company_id;
perform set_config('app.procore_audit_delete_allowed', 'off', true);
get diagnostics v_deleted = row_count;
return v_deleted;
exception
when others then
perform set_config('app.procore_audit_delete_allowed', 'off', true);
raise;
end;
$$;

Expand Down
9 changes: 9 additions & 0 deletions tests/test_procore_audit_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ def test_append_only_triggers():
assert "before update on private.procore_audit" in sql
assert "before delete on private.procore_audit" in sql
assert "append-only" in sql
assert "set search_path = ''" in sql


def test_controlled_delete_guard():
sql = _sql()
assert "app.procore_audit_delete_allowed" in sql
assert "current_setting('app.procore_audit_delete_allowed', true)" in sql
assert "set_config('app.procore_audit_delete_allowed', 'on', true)" in sql
assert "set_config('app.procore_audit_delete_allowed', 'off', true)" in sql


def test_retention_and_deletion_functions():
Expand Down
Loading