Skip to content

[codex] Add actor attribution to audit logs#2462

Open
riderx wants to merge 5 commits into
mainfrom
codex/audit-log-actor-attribution
Open

[codex] Add actor attribution to audit logs#2462
riderx wants to merge 5 commits into
mainfrom
codex/audit-log-actor-attribution

Conversation

@riderx

@riderx riderx commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Added audit log actor snapshot fields for user, API key, and automated/system actions.
  • Removed audit log foreign keys to users and orgs so retained audit rows survive deleted resources.
  • Updated the audit log trigger, webhook payloads, API schema, generated types, UI display, and tests.

Motivation (AI generated)

Audit logs previously only stored a user id, which made it hard to distinguish direct user actions from API key actions or automated backend actions. The old foreign keys could also erase or mutate attribution when dependent resources were deleted.

Business Impact (AI generated)

This improves incident investigation and customer support by making audit trails clearer and more durable. Teams can identify whether an action came from a user, an API key id/name, or automation without exposing API key secrets.

Test Plan (AI generated)

  • bun lint
  • bun typecheck
  • bun run supabase:with-env -- bunx vitest run tests/audit-logs.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/audit-logs.test.ts tests/webhook-delivery-security.unit.test.ts tests/webhook-delivery-redirect.unit.test.ts tests/webhook-queue-processing.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/organization-api.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/chart-refresh-rpc.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/webhook-signature.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/bundle.test.ts
  • bun test:all was run twice locally; each run had one unrelated parallel-suite failure, and each failed file passed when rerun alone.

Generated with AI

Summary by CodeRabbit

  • New Features
    • Audit logs now display comprehensive actor information showing who made each change—users (with email), API keys (with ID and name), or system events
    • Enhanced audit trail accuracy with improved retention of historical snapshots, even when related organization or user records are deleted

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces audit log actor attribution, tracking whether changes originated from a user, API key, or system, plus org-id tombstoning to prevent UUID reuse while retaining audit history. The backend migration auto-generates audit logs via triggers; the frontend displays actor information in a new column and modal section.

Changes

Audit Log Actor Attribution

Layer / File(s) Summary
Schema and type contracts
src/types/supabase.types.ts, supabase/functions/_backend/utils/supabase.types.ts
audit_logs.Row/Insert/Update extended with actor_type, actor_user_id, actor_user_email, actor_apikey_id, actor_apikey_name fields. New org_id_tombstones table type added with org_id and deleted_at. audit_logs.Relationships changed from foreign-key list to empty array.
Database migration and trigger functions
supabase/migrations/20260608160711_audit_log_actor_attribution.sql
Creates org_id_tombstones with RLS, backfills tombstones for orphaned org ids, drops audit_logs foreign keys, adds actor attribution columns and check constraint, backfills actor fields from legacy data, adds indexes. Defines prevent_org_id_reuse() and lock_org_tombstone_guard() triggers to block org-id updates/reuse. Adds tombstone_deleted_org_id() trigger on org delete. Defines audit_log_trigger() to auto-generate audit logs from DML on tracked tables with actor attribution resolved from API key header or auth.uid(). Adds trigger_webhook_on_audit_log() to enqueue webhook events via pgmq. Introduces delete_accounts_marked_for_deletion(SECURITY DEFINER) to delete expired accounts with org membership transfer logic.
Webhook and backend audit integration
supabase/functions/_backend/utils/webhook.ts, supabase/functions/_backend/public/organization/audit.ts
WebhookPayload.data and AuditLogData extended with optional actor fields (actor_type, actor_user_id, actor_user_email, actor_apikey_id, actor_apikey_name). buildWebhookPayload maps audit-log actor fields into payload with fallback for actor_type. Audit log schema validation expanded to parse actor fields.
Frontend audit log display and localization
src/components/tables/AuditLogTable.vue, messages/en.json
AuditLogTable extends AuditLogRow with actor metadata, adds actor column to table, implements actor-display resolver with format mapping for API keys/system/users, member enrichment now looks up by actor_user_id preferentially. Details modal replaces email display with "source" block showing actor type and conditional API key id/name and actor user email. I18n keys added: actor, actor-user-email, api-key-id, api-key-name, automated, source.
Database seeding updates
supabase/seed.sql
reset_and_seed_data() truncates org_id_tombstones. reset_and_seed_app_data() deletes tombstone entries for target org before seeding.
Test coverage and actor attribution validation
tests/audit-logs.test.ts, supabase/tests/29_test_delete_accounts_marked_for_deletion.sql, supabase/tests/40_test_audit_log_apikey.sql
audit-logs.test.ts extended with actor attribution schema and interface fields, test setup loads user email for assertions, new test validates audit-log snapshot retention without foreign keys, org-update and app-version tests assert actor fields for API-key and system actors. delete_accounts test updates audit log inserts and post-deletion assertions to verify retained user/email snapshots. apikey test adds org-id-tombstone lifecycle tests: trigger validation, forbidden update/reuse, tombstone write on delete.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Cap-go/capgo#1975: Updates tests/audit-logs.test.ts API key authentication test setup, directly related to audit-log test infrastructure changes in this PR.

Suggested labels

codex

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding actor attribution to audit logs, which is the central feature across all modified files.
Description check ✅ Passed The description includes a summary (AI-generated), motivation, business impact, and a detailed test plan with checkbox verification. All key sections from the template are substantially addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (4.2.1)
supabase/tests/40_test_audit_log_apikey.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica

supabase/migrations/20260608160711_audit_log_actor_attribution.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica

supabase/tests/29_test_delete_accounts_marked_for_deletion.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq

codspeed-hq Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/audit-log-actor-attribution (8f2effe) with main (1d9a54c)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@riderx riderx marked this pull request as ready for review June 8, 2026 17:04
@riderx

riderx commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c26343e090

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260608160711_audit_log_actor_attribution.sql

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 478246fcda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260608160711_audit_log_actor_attribution.sql

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f2effed3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +92 to +95
UPDATE "public"."audit_logs"
SET
"actor_type" = CASE WHEN "user_id" IS NULL THEN 'unknown' ELSE 'user' END,
"actor_user_id" = "user_id"

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 Mark legacy API-key audit rows as unknown

For historical rows created before actor_type existed, a non-null user_id does not prove a human user made the change: the old trigger populated user_id from get_identity(), which also returned the API-key owner for API-key requests. With this backfill, existing API-key mutations are permanently shown as actor_type = 'user' in the audit UI/API instead of unknown or otherwise distinguishable, so customers reviewing retained audit logs will see misleading actor attribution for all pre-migration API-key activity.

Useful? React with 👍 / 👎.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/tables/AuditLogTable.vue`:
- Around line 253-255: The getActorUserEmail helper currently returns
actor_user_email or user?.email unconditionally; change
getActorUserEmail(ExtendedAuditLog) so it only returns those values when the
audit item represents a user actor (e.g., item.actor_type === 'user'), otherwise
return null; likewise update places that fallback to actor_user_id / user_id
(calls/usages of getActorUserEmail and any inline fallbacks) to only use those
user-specific fields when actor_type === 'user' to prevent showing user
emails/IDs for non-user actors (API key/system).

In `@supabase/migrations/20260608160711_audit_log_actor_attribution.sql`:
- Around line 358-500: The SECURITY DEFINER function
public.delete_accounts_marked_for_deletion() is currently executable by any
role; revoke public execute and explicitly grant EXECUTE only to trusted
roles/groups (e.g., your admin role(s)) to prevent privilege escalation. Add
statements after the function definition that REVOKE EXECUTE ON FUNCTION
public.delete_accounts_marked_for_deletion() FROM PUBLIC; and then GRANT EXECUTE
ON FUNCTION public.delete_accounts_marked_for_deletion() TO <trusted_role1>[,
<trusted_role2>] (replace with your actual admin/trusted role names), ensuring
the function remains owned by postgres.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68f48908-b20b-4600-ad4e-1b872de3c325

📥 Commits

Reviewing files that changed from the base of the PR and between 1d9a54c and 8f2effe.

📒 Files selected for processing (11)
  • messages/en.json
  • src/components/tables/AuditLogTable.vue
  • src/types/supabase.types.ts
  • supabase/functions/_backend/public/organization/audit.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/functions/_backend/utils/webhook.ts
  • supabase/migrations/20260608160711_audit_log_actor_attribution.sql
  • supabase/seed.sql
  • supabase/tests/29_test_delete_accounts_marked_for_deletion.sql
  • supabase/tests/40_test_audit_log_apikey.sql
  • tests/audit-logs.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment on lines +253 to +255
function getActorUserEmail(item: ExtendedAuditLog): string | null {
return item.actor_user_email || item.user?.email || null
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict member/email fallback to user actors only.

Current fallback (actor_user_id || user_id + unconditional getActorUserEmail(...) render) can display user email for non-user actors (API key/system), which conflicts with the actor attribution model and can mislead incident/support analysis.

Proposed fix
 function getActorUserEmail(item: ExtendedAuditLog): string | null {
-  return item.actor_user_email || item.user?.email || null
+  if (item.actor_type !== 'user')
+    return null
+  return item.actor_user_email || item.user?.email || null
 }
@@
     const rows = (data ?? []) as ExtendedAuditLog[]

     for (const item of rows) {
-      const memberId = item.actor_user_id || item.user_id
+      const memberId = item.actor_type === 'user'
+        ? (item.actor_user_id || item.user_id)
+        : null
       if (memberId) {
         const member = membersMap.value.get(memberId)
         if (member) {
           item.user = member
         }
       }
     }

Also applies to: 356-357, 637-640

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/tables/AuditLogTable.vue` around lines 253 - 255, The
getActorUserEmail helper currently returns actor_user_email or user?.email
unconditionally; change getActorUserEmail(ExtendedAuditLog) so it only returns
those values when the audit item represents a user actor (e.g., item.actor_type
=== 'user'), otherwise return null; likewise update places that fallback to
actor_user_id / user_id (calls/usages of getActorUserEmail and any inline
fallbacks) to only use those user-specific fields when actor_type === 'user' to
prevent showing user emails/IDs for non-user actors (API key/system).

Comment on lines +358 to +500
CREATE OR REPLACE FUNCTION "public"."delete_accounts_marked_for_deletion"() RETURNS TABLE("deleted_count" integer, "deleted_user_ids" "uuid"[])
LANGUAGE "plpgsql" SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
account_record RECORD;
org_record RECORD;
deleted_users UUID[] := ARRAY[]::UUID[];
total_deleted INTEGER := 0;
other_super_admins_count INTEGER;
replacement_owner_id UUID;
BEGIN
-- Loop through all accounts marked for deletion where removal_date has passed
FOR account_record IN
SELECT "account_id", "removal_date", "removed_data"
FROM "public"."to_delete_accounts"
WHERE "removal_date" < NOW()
LOOP
BEGIN
-- Process each org the user belongs to
FOR org_record IN
SELECT DISTINCT "org_id", "user_right"
FROM "public"."org_users"
WHERE "user_id" = account_record.account_id
LOOP
-- Reset replacement_owner_id for each org
replacement_owner_id := NULL;

-- Check if user is a super_admin in this org
IF org_record.user_right = 'super_admin'::"public"."user_min_right" THEN
-- Count other super_admins in this org (excluding the user being deleted)
SELECT COUNT(*) INTO other_super_admins_count
FROM "public"."org_users"
WHERE "org_id" = org_record.org_id
AND "user_id" != account_record.account_id
AND "user_right" = 'super_admin'::"public"."user_min_right";

IF other_super_admins_count = 0 THEN
-- User is the last super_admin: DELETE all org resources
RAISE NOTICE 'User % is last super_admin of org %. Deleting all org resources.',
account_record.account_id, org_record.org_id;

-- Delete deploy_history for this org
DELETE FROM "public"."deploy_history" WHERE "owner_org" = org_record.org_id;

-- Delete channel_devices for this org
DELETE FROM "public"."channel_devices" WHERE "owner_org" = org_record.org_id;

-- Delete channels for this org
DELETE FROM "public"."channels" WHERE "owner_org" = org_record.org_id;

-- Delete app_versions for this org
DELETE FROM "public"."app_versions" WHERE "owner_org" = org_record.org_id;

-- Delete apps for this org
DELETE FROM "public"."apps" WHERE "owner_org" = org_record.org_id;

-- Delete the org itself since user is last super_admin. Audit logs
-- intentionally keep their org_id snapshot without a foreign key.
DELETE FROM "public"."orgs" WHERE "id" = org_record.org_id;

-- Skip ownership transfer since all resources are deleted
CONTINUE;
END IF;
END IF;

-- If we reach here, we need to transfer ownership (either non-super_admin or non-last super_admin)
-- Find a super_admin to transfer ownership to
SELECT "user_id" INTO replacement_owner_id
FROM "public"."org_users"
WHERE "org_id" = org_record.org_id
AND "user_id" != account_record.account_id
AND "user_right" = 'super_admin'::"public"."user_min_right"
LIMIT 1;

IF replacement_owner_id IS NOT NULL THEN
RAISE NOTICE 'Transferring ownership from user % to user % in org %',
account_record.account_id, replacement_owner_id, org_record.org_id;

-- Transfer app ownership
UPDATE "public"."apps"
SET "user_id" = replacement_owner_id, "updated_at" = NOW()
WHERE "user_id" = account_record.account_id AND "owner_org" = org_record.org_id;

-- Transfer app_versions ownership
UPDATE "public"."app_versions"
SET "user_id" = replacement_owner_id, "updated_at" = NOW()
WHERE "user_id" = account_record.account_id AND "owner_org" = org_record.org_id;

-- Transfer channels ownership
UPDATE "public"."channels"
SET "created_by" = replacement_owner_id, "updated_at" = NOW()
WHERE "created_by" = account_record.account_id AND "owner_org" = org_record.org_id;

-- Transfer deploy_history ownership
UPDATE "public"."deploy_history"
SET "created_by" = replacement_owner_id, "updated_at" = NOW()
WHERE "created_by" = account_record.account_id AND "owner_org" = org_record.org_id;

-- Transfer org ownership if user created it
UPDATE "public"."orgs"
SET "created_by" = replacement_owner_id, "updated_at" = NOW()
WHERE "id" = org_record.org_id AND "created_by" = account_record.account_id;
ELSE
RAISE WARNING 'No super_admin found to transfer ownership in org % for user %',
org_record.org_id, account_record.account_id;
END IF;
END LOOP;

-- Delete from public.users table
DELETE FROM "public"."users" WHERE "id" = account_record.account_id;

-- Delete from auth.users table
DELETE FROM "auth"."users" WHERE "id" = account_record.account_id;

-- Remove from to_delete_accounts table
DELETE FROM "public"."to_delete_accounts" WHERE "account_id" = account_record.account_id;

-- Track the deleted user
deleted_users := "array_append"(deleted_users, account_record.account_id);
total_deleted := total_deleted + 1;

-- Log the deletion
RAISE NOTICE 'Successfully deleted account: % (removal date: %)',
account_record.account_id, account_record.removal_date;

EXCEPTION
WHEN OTHERS THEN
-- Log the error but continue with other accounts
RAISE WARNING 'Failed to delete account %: %', account_record.account_id, SQLERRM;
END;
END LOOP;

-- Return results
deleted_count := total_deleted;
deleted_user_ids := deleted_users;
RETURN NEXT;

RAISE NOTICE 'Deletion process completed. Total accounts deleted: %', total_deleted;
END;
$$;

ALTER FUNCTION "public"."delete_accounts_marked_for_deletion"() OWNER TO "postgres";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
psql "$DATABASE_URL" -X -A -F $'\t' -c "
SELECT grantee, privilege_type
FROM information_schema.routine_privileges
WHERE routine_schema = 'public'
  AND routine_name = 'delete_accounts_marked_for_deletion'
ORDER BY grantee, privilege_type;"

Repository: Cap-go/capgo

Length of output: 101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/migrations/20260608160711_audit_log_actor_attribution.sql"

# Ensure file exists
ls -l "$FILE"

# Show nearby parts where the function is defined and where grants/revokes might appear
rg -n "delete_accounts_marked_for_deletion" "$FILE"

# Extract a window around the function end/ALTER FUNCTION line(s)
# (Use line numbers from rg output to focus; fallback to approximate range.)
python3 - <<'PY'
import re
path="supabase/migrations/20260608160711_audit_log_actor_attribution.sql"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
# find indices containing function name
idx=[i for i,l in enumerate(lines) if "delete_accounts_marked_for_deletion" in l]
print("matches:", idx)
if idx:
    i=idx[-1]
    start=max(0,i-120)
    end=min(len(lines), i+120)
    for j in range(start,end):
        print(f"{j+1}\t{lines[j].rstrip()}")
PY

# Also search for grants/revokes for that specific function name in the repo migration file
rg -n "GRANT EXECUTE|REVOKE ALL ON FUNCTION|REVOKE EXECUTE|GRANT ON FUNCTION" "$FILE" || true

# Also check whether there are any generic post-creation GRANT/REVOKE patterns for SECURITY DEFINER functions in this migration
rg -n "SECURITY DEFINER|REVOKE|GRANT" "$FILE" | head -n 200

Repository: Cap-go/capgo

Length of output: 7231


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="supabase/migrations/20260608160711_audit_log_actor_attribution.sql"

# Show every line that mentions the function name
rg -n "delete_accounts_marked_for_deletion" "$FILE" -n

# Search for any grant/revoke statements that might not match the earlier regexes
rg -n "GRANT .*EXECUTE|REVOKE .*EXECUTE|GRANT .*ON FUNCTION|REVOKE .*ON FUNCTION" "$FILE" || true

# Search for the function name within a small window after any GRANT/REVOKE keyword occurrences
python3 - <<'PY'
import re
path="supabase/migrations/20260608160711_audit_log_actor_attribution.sql"
with open(path,'r',encoding='utf-8') as f:
    lines=f.readlines()
hits=[]
keywords=re.compile(r'\b(GRANT|REVOKE)\b', re.I)
for i,l in enumerate(lines):
    if keywords.search(l):
        window="".join(lines[max(0,i-3):min(len(lines),i+10)])
        if "delete_accounts_marked_for_deletion" in window:
            hits.append(i+1)
print("grant/revoke windows containing function name (line numbers):", hits)
PY

Repository: Cap-go/capgo

Length of output: 833


Restrict execution of public.delete_accounts_marked_for_deletion() to trusted roles.

public.delete_accounts_marked_for_deletion() is a SECURITY DEFINER destructive function, but this migration adds no REVOKE ... FROM PUBLIC / scoped GRANT EXECUTE for it (unlike other SECURITY DEFINER functions in the same file), so unintended callers may be able to execute it with elevated privileges.

🔒 Proposed fix
 ALTER FUNCTION "public"."delete_accounts_marked_for_deletion"() OWNER TO "postgres";
+REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion"() FROM PUBLIC;
+REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion"() FROM "anon";
+REVOKE ALL ON FUNCTION "public"."delete_accounts_marked_for_deletion"() FROM "authenticated";
+GRANT EXECUTE ON FUNCTION "public"."delete_accounts_marked_for_deletion"() TO "service_role";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260608160711_audit_log_actor_attribution.sql` around
lines 358 - 500, The SECURITY DEFINER function
public.delete_accounts_marked_for_deletion() is currently executable by any
role; revoke public execute and explicitly grant EXECUTE only to trusted
roles/groups (e.g., your admin role(s)) to prevent privilege escalation. Add
statements after the function definition that REVOKE EXECUTE ON FUNCTION
public.delete_accounts_marked_for_deletion() FROM PUBLIC; and then GRANT EXECUTE
ON FUNCTION public.delete_accounts_marked_for_deletion() TO <trusted_role1>[,
<trusted_role2>] (replace with your actual admin/trusted role names), ensuring
the function remains owned by postgres.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant