Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Migration: Prevent API keys from being created for organization accounts
-- An account is an "organization" if it exists in the organization_id column
-- of the account_organization_ids table. API keys should only be issued to
-- individual member accounts, never to the org account itself.

CREATE OR REPLACE FUNCTION public.prevent_org_account_api_keys()
RETURNS trigger AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM public.account_organization_ids
WHERE organization_id = NEW.account
) THEN
RAISE EXCEPTION
'Cannot create an API key for an organization account (account_id: %)',
NEW.account;
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Comment on lines +6 to +21
Copy link
Copy Markdown

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
set -euo pipefail

echo "== RLS/policies/grants touching account_organization_ids =="
rg -n -C3 'account_organization_ids|ENABLE ROW LEVEL SECURITY|CREATE POLICY|GRANT .*account_organization_ids' supabase/migrations

echo
echo "== Function security/search_path declarations =="
rg -n -C3 'prevent_org_account_api_keys|SECURITY DEFINER|search_path' supabase/migrations

Repository: recoupable/database

Length of output: 25027


🏁 Script executed:

cat -n supabase/migrations/20260319000000_prevent_org_account_api_keys.sql

Repository: recoupable/database

Length of output: 1096


🏁 Script executed:

# Search for any CREATE POLICY statements on account_organization_ids more explicitly
rg -n 'CREATE POLICY.*account_organization_ids|CREATE POLICY' supabase/migrations/20251204000000_create_account_organization_ids.sql

Repository: recoupable/database

Length of output: 45


🏁 Script executed:

# Check if there are RLS policies defined elsewhere (policies might be in separate migration)
rg -n 'account_organization_ids.*CREATE POLICY|CREATE POLICY.*account_organization_ids|ON.*account_organization_ids' supabase/migrations

Repository: recoupable/database

Length of output: 2180


🏁 Script executed:

# Check for GRANT statements on account_organization_ids
rg -n 'GRANT.*account_organization_ids|GRANT.*SELECT.*account_organization_ids' supabase/migrations

Repository: recoupable/database

Length of output: 45


🏁 Script executed:

# Check if account_organization_ids has any RLS policies defined anywhere
rg -n 'account_organization_ids' supabase/migrations | grep -i 'policy\|grant\|enabled'

Repository: recoupable/database

Length of output: 45


🏁 Script executed:

# Look for the entire content of the account_organization_ids creation migration
cat -n supabase/migrations/20251204000000_create_account_organization_ids.sql | tail -20

Repository: recoupable/database

Length of output: 1134


🏁 Script executed:

# Check if there are any RLS-related files or separate policy migrations
fd -t f 'policy\|rls' supabase/migrations/

Repository: recoupable/database

Length of output: 45


🏁 Script executed:

# Search for all CREATE POLICY statements across entire migrations directory
rg -n '^CREATE POLICY' supabase/migrations/

Repository: recoupable/database

Length of output: 435


Function will silently fail to enforce the check for non-superuser roles due to missing RLS policy coverage.

The account_organization_ids table has RLS enabled but no policies defined. Combined with SECURITY INVOKER (the default), the function's SELECT query will return zero rows when executed by an application-level authenticated role, causing the validation to always pass silently.

To fix: Either (1) add SECURITY DEFINER SET search_path = pg_catalog, public to the function and ensure the triggering role has necessary grants, or (2) define an explicit RLS policy on account_organization_ids that permits SELECT access to the roles that will execute this trigger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260319000000_prevent_org_account_api_keys.sql` around
lines 6 - 21, The trigger function prevent_org_account_api_keys currently runs
with default SECURITY INVOKER and will see zero rows when
account_organization_ids has RLS without policies; to fix, either convert the
function to SECURITY DEFINER and set an explicit search_path (e.g., add
"SECURITY DEFINER SET search_path = pg_catalog, public") and ensure the definer
role has SELECT rights on account_organization_ids, or keep SECURITY INVOKER but
add an RLS policy on account_organization_ids that allows SELECT for the
application roles executing the trigger; update the function declaration or RLS
policy accordingly and verify the definer/role permissions so the SELECT in
prevent_org_account_api_keys reliably sees rows.


CREATE TRIGGER prevent_org_account_api_keys_trigger
BEFORE INSERT OR UPDATE ON public.account_api_keys
FOR EACH ROW
EXECUTE FUNCTION public.prevent_org_account_api_keys();