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
163 changes: 163 additions & 0 deletions docs/SECURITY-DEFINER-PATTERN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Padrão SECURITY DEFINER + RLS no PromoGifts

**Status:** vigente desde Onda 20 (15/mai/2026)
**Gate:** `scripts/check-security-definer-acl.mjs` + `audit_security_definer_acl()` RPC
**Aplica-se a:** todas as funções `SECURITY DEFINER` em `public`

---

## TL;DR — checklist por padrão

Toda função `SECURITY DEFINER` em `public` cai em uma de 3 categorias. Escolha a categoria correta antes de escrever a migration:

| Categoria | Quando usar | GRANT EXECUTE para |
|---|---|---|
| **A. RLS helper** | Função usada em `pg_policies` (em `qual` ou `with_check`) | `authenticated`, `service_role` |
| **B. RPC backend-only** | Função chamada só por edge function via `service_role` | `service_role` (apenas) |
| **C. Public-intent** | Endpoint público intencional (ex: validação de token de aprovação) | `anon`, `authenticated`, `service_role` + adicionar nome na whitelist de `audit_security_definer_acl()` |

Em **TODOS** os casos, **PUBLIC nunca tem EXECUTE** (revoke default do PostgreSQL).

---

## Por que isso importa

PostgreSQL define duas coisas independentes:

1. **Quem executa a função** (`SECURITY DEFINER` = como owner; `SECURITY INVOKER` = como caller).
2. **Quem pode chamar a função** (grants em `pg_proc.proacl`).

A confusão comum: pensar que `SECURITY DEFINER` dispensa o GRANT do caller. **Não dispensa.** O caller ainda precisa de `EXECUTE` pra invocar. Se uma RLS policy faz `WHERE is_admin_or_above(auth.uid())` e `authenticated` não tem EXECUTE em `is_admin_or_above`, qualquer SELECT/UPDATE/INSERT/DELETE nessa tabela quebra com:

```
ERROR: 42501: permission denied for function is_admin_or_above
```
Comment on lines +32 to +34
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 | 🟡 Minor | ⚡ Quick win

Defina linguagem no bloco cercado para eliminar warning de lint.

O bloco de erro está sem linguagem e dispara MD040.

💡 Ajuste sugerido
-```
+```text
 ERROR: 42501: permission denied for function is_admin_or_above
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 32-32: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

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

In @docs/SECURITY-DEFINER-PATTERN.md around lines 32 - 34, O bloco de erro no
documento está sem linguagem e aciona o lint MD040; edite o fence que contém
"ERROR: 42501: permission denied for function is_admin_or_above" para declarar a
linguagem, por exemplo trocando paratext de forma que o bloco comece com
text e termine com , garantindo que o código seja tratado como texto e o
warning seja eliminado.


</details>

<!-- fingerprinting:phantom:poseidon:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->


Esse foi o bug do PR #192 (migration `t38_deploy_hardening_final`): aplicou REVOKE FROM authenticated em 2 funções RLS helper, quebrou 112 policies. Onda 20 corrigiu e adicionou gate bilateral.

---

## Padrão A — RLS helper (mais comum no PromoGifts)

**Exemplos no repo:** `is_admin_or_above`, `is_coord_or_above`, `is_supervisor_or_above`, `has_org_role`, `org_has_any_members`, `can_access_quote`.

**Template canônico:**

```sql
CREATE OR REPLACE FUNCTION public.minha_funcao_rls(_user_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public, pg_catalog
AS $$
SELECT EXISTS (
SELECT 1 FROM public.user_roles
WHERE user_id = _user_id AND role IN ('supervisor', 'dev')
);
$$;

-- Hardening obrigatório:
REVOKE EXECUTE ON FUNCTION public.minha_funcao_rls(uuid) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION public.minha_funcao_rls(uuid) FROM anon;
GRANT EXECUTE ON FUNCTION public.minha_funcao_rls(uuid) TO authenticated;
GRANT EXECUTE ON FUNCTION public.minha_funcao_rls(uuid) TO service_role;
```

> O comentário `-- rls-helper:` opcional satisfaz o regex do gate `check-security-definer-hardening.mjs` quando a função não tem REVOKE explícito de anon (caso raro). Prefira o REVOKE explícito.

---

## Padrão B — RPC backend-only

**Exemplos no repo:** funções chamadas só por edge functions via service_role (ex: `revoke_all_user_tokens`).

**Template canônico:**

```sql
CREATE OR REPLACE FUNCTION public.minha_rpc_admin(_arg text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_catalog
AS $$
BEGIN
-- lógica privilegiada
RETURN jsonb_build_object('ok', true);
END;
$$;

REVOKE EXECUTE ON FUNCTION public.minha_rpc_admin(text) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION public.minha_rpc_admin(text) FROM anon;
REVOKE EXECUTE ON FUNCTION public.minha_rpc_admin(text) FROM authenticated;
-- service_role mantém grant default
```

Não é citada em nenhuma policy. Se for, vira Padrão A.

---

## Padrão C — Public-intent

**Exemplos no repo:** `submit_quote_response`, `get_quote_token_by_value` (validam token público).

**Template canônico:**

```sql
CREATE OR REPLACE FUNCTION public.validate_public_token(_token text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_catalog
AS $$
BEGIN
-- valida token, rate-limit interno, etc
END;
$$;

REVOKE EXECUTE ON FUNCTION public.validate_public_token(text) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION public.validate_public_token(text) TO anon;
GRANT EXECUTE ON FUNCTION public.validate_public_token(text) TO authenticated;
GRANT EXECUTE ON FUNCTION public.validate_public_token(text) TO service_role;
```

**E adicione o nome na whitelist** em `audit_security_definer_acl()`:

```sql
(p.proname IN ('submit_quote_response', 'get_quote_token_by_value', 'validate_public_token')) AS public_intent
```

Sem esse passo, o gate ACL falha mesmo o grant sendo intencional.

---

## O gate `audit_security_definer_acl()` — 4 categorias detectadas

| Caso | Anti-padrão | Mensagem |
|---|---|---|
| 1 | PUBLIC com EXECUTE | `PUBLIC has EXECUTE` |
| 2 | anon com EXECUTE fora da whitelist | `anon has EXECUTE (not in public-intent whitelist)` |
| 3 | trigger function com EXECUTE pra authenticated (sem sentido — triggers são invocadas pelo PG) | `trigger function has EXECUTE for authenticated` |
| 4 | função citada em `pg_policies` sem EXECUTE pra authenticated → RLS quebra com 42501 | `used in RLS policy but missing EXECUTE for authenticated (RLS will fail with 42501)` |

O CI roda o RPC após cada deploy. Qualquer linha retornada → gate falha.

---

## Workflow recomendado pra criar nova função SECURITY DEFINER

1. **Decidir categoria** (A/B/C) antes de escrever.
2. **Copiar template** correspondente acima.
3. **Substituir nome/args/lógica.**
4. **Manter** `SET search_path = public, pg_catalog` (sem isso, search_path injection vira CVE).
5. **Manter** REVOKE FROM PUBLIC e GRANTs explícitos do template.
6. **Se for Padrão C**, atualizar whitelist da função `audit_security_definer_acl()` na mesma migration.
7. **Rodar localmente** (se Supabase CLI disponível): `supabase migration up` → executa também o `RAISE EXCEPTION` da Onda 20 e aborta se houver violação.

---

## Histórico

- **Onda 20 (15/mai/2026, PR #TBD):** gate bilateral. Adiciona Caso 4. Corrige regression do t38 (PR #192) e gap do org_has_any_members.
- **t28 pilot (12/mai/2026, PRs #205-#212):** REVOKE de anon/PUBLIC nas funções admin existentes (257 ocorrências fechadas).
- **Hardening original (27/abr/2026):** criação inicial de `audit_security_definer_acl()` com Casos 1, 2, 3.
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
-- =================================================================
-- Onda 20: Fix t38 RLS regression + bilateral SECURITY DEFINER gate
--
-- Contexto:
-- PR #192 (migration t38_deploy_hardening_final) aplicou
-- princípio do menor privilégio sem checar uso: revogou EXECUTE
-- de is_admin_or_above(uuid) e is_coord_or_above(uuid) do role
-- 'authenticated'. Mas essas funções são chamadas em 83+29 RLS
-- policies por usuários authenticated, então toda operação
-- logada quebrou com `42501: permission denied for function ...`.
--
-- Adicionalmente, migration 20260514000000 criou
-- org_has_any_members(uuid) sem REVOKE FROM PUBLIC/anon — gate
-- audit_security_definer_acl() começou a falhar.
--
-- Causa raiz dupla:
-- 1. Erro humano em t38: REVOKE indevido em funções RLS helper.
-- 2. Erro arquitetural: gate atual é unilateral — detecta
-- excesso de privilégio (PUBLIC, anon), mas não detecta
-- CARÊNCIA (função SECURITY DEFINER em policy sem EXECUTE
-- pra authenticated). Por isso t38 passou pré-merge.
--
-- O que esta migration faz:
-- 1. RE-GRANT is_admin_or_above e is_coord_or_above pra authenticated
-- (desbloqueia 112 policies imediatamente)
-- 2. REVOKE org_has_any_members de PUBLIC e anon
-- (mantém authenticated + service_role; policy precisa)
-- 3. Reescreve audit_security_definer_acl() com 4 categorias:
-- a) PUBLIC has EXECUTE
-- b) anon EXECUTE fora da whitelist
-- c) trigger function com EXECUTE pra authenticated
-- d) NOVO: SECURITY DEFINER citada em pg_policy sem EXECUTE
-- pra authenticated (RLS quebra com 42501)
-- 4. RAISE EXCEPTION fail-fast se sobrar violação ao final
-- (rollback automático garante atomicidade)
--
-- Padrão documentado em docs/SECURITY-DEFINER-PATTERN.md
-- =================================================================

-- ─────────────────────────────────────────────────────────────────
-- 1. RE-GRANT funções quebradas pelo t38 (Bloco 1 da t38)
-- ─────────────────────────────────────────────────────────────────
GRANT EXECUTE ON FUNCTION public.is_admin_or_above(_user_id uuid) TO authenticated;
GRANT EXECUTE ON FUNCTION public.is_coord_or_above(_user_id uuid) TO authenticated;
Comment on lines +43 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard the role-helper grants

In any preview or rebuilt environment that does not already have these legacy helper functions, these unconditional GRANT EXECUTE ON FUNCTION ... statements abort with “function does not exist” before the migration reaches the new ACL gate. The preceding t38 migration used pg_proc checks before touching the same functions, so this repair migration needs the same guard or dynamic SQL to remain safe for the partial schemas this migration set already supports.

Useful? React with 👍 / 👎.


-- ─────────────────────────────────────────────────────────────────
-- 2. REVOKE excesso em org_has_any_members
-- (criada por 20260514000000 com default PUBLIC do PostgreSQL)
-- ─────────────────────────────────────────────────────────────────
REVOKE EXECUTE ON FUNCTION public.org_has_any_members(_org_id uuid) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION public.org_has_any_members(_org_id uuid) FROM anon;
Comment on lines +50 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard the org_has_any_members revoke

In preview/partial schemas where 20260514000000_fix_policy_idempotency_and_security.sql skipped creating org_has_any_members because organization_members did not exist (that migration wraps the function creation in an IF EXISTS table check), this unconditional REVOKE EXECUTE ON FUNCTION public.org_has_any_members(...) aborts with “function does not exist” before the rest of the migration can run. The surrounding migrations are explicitly defensive for those environments, so this fix should also check pg_proc or use dynamic guarded SQL before revoking from this function.

Useful? React with 👍 / 👎.


-- ─────────────────────────────────────────────────────────────────
-- 3. Gate bilateral: reescreve audit_security_definer_acl()
-- para também reportar funções em policy sem EXECUTE pra
-- authenticated (Caso 4).
-- ─────────────────────────────────────────────────────────────────
CREATE OR REPLACE FUNCTION public.audit_security_definer_acl()
RETURNS TABLE(
function_name TEXT,
arguments TEXT,
problem TEXT,
granted_to TEXT
)
LANGUAGE sql
STABLE
SECURITY INVOKER
SET search_path = public, pg_catalog
AS $$
WITH defs AS (
SELECT
p.oid,
p.proname,
pg_get_function_identity_arguments(p.oid) AS args,
p.proacl,
(pg_get_function_result(p.oid) = 'trigger') AS is_trigger,
-- whitelist de funções intencionalmente acessíveis a anon
(p.proname IN ('submit_quote_response', 'get_quote_token_by_value')) AS public_intent
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public' AND p.prosecdef = 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.

P1 Badge Include invoker RLS helpers in the missing-grant gate

Because defs is restricted to p.prosecdef = true, Case 4 never inspects SECURITY INVOKER functions used by RLS policies. In this repo 20260513000001_t37a_security_invoker_safe_batch.sql already converted both is_admin_or_above(uuid) and is_coord_or_above(uuid) to SECURITY INVOKER, so this new gate would not have caught the exact t38 revocation regression it is meant to prevent if the explicit re-grants were removed or a future migration repeats the revoke.

Useful? React with 👍 / 👎.

),
acl_expanded AS (
SELECT
d.oid,
d.proname,
d.args,
d.is_trigger,
d.public_intent,
a.grantee::regrole::text AS grantee
FROM defs d
LEFT JOIN LATERAL (
SELECT (aclexplode(d.proacl)).grantee
) a ON true
WHERE a.grantee IS NOT NULL
),
Comment on lines +91 to +96
Comment on lines +83 to +96
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files "supabase/migrations/*.sql" | head -20

Repository: adm01-debug/Promo_Gifts

Length of output: 1161


🏁 Script executed:

cat -n supabase/migrations/20260515150000_onda20_fix_t38_regression_and_bilateral_gate.sql | head -150

Repository: adm01-debug/Promo_Gifts

Length of output: 7627


🏁 Script executed:

# Verificar tamanho do arquivo para leitura completa
wc -l supabase/migrations/20260515150000_onda20_fix_t38_regression_and_bilateral_gate.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 155


🏁 Script executed:

cat -n supabase/migrations/20260515150000_onda20_fix_t38_regression_and_bilateral_gate.sql | tail -30

Repository: adm01-debug/Promo_Gifts

Length of output: 1743


🏁 Script executed:

# Verificar se existem funções SECURITY DEFINER no public com proacl NULL
# usando readymade queries PostgreSQL (syntax check)
cat > /tmp/test_acl_logic.sql << 'EOF'
-- Teste 1: aclexplode() com NULL
SELECT COUNT(*) FROM aclexplode(NULL::aclitem[]);

-- Teste 2: acldefault() para função
SELECT acldefault('f'::char, 1::oid);

-- Teste 3: Comparar resultados
WITH test_null AS (
  SELECT (aclexplode(NULL::aclitem[])).grantee AS grantee_null
),
test_default AS (
  SELECT (aclexplode(acldefault('f'::char, 1::oid))).grantee AS grantee_default
)
SELECT 
  (SELECT COUNT(*) FROM test_null) AS null_count,
  (SELECT COUNT(*) FROM test_default) AS default_count;
EOF
cat /tmp/test_acl_logic.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 560


🏁 Script executed:

# Verificar a doc PostgreSQL: aclexplode() behavior
# Além disso, verificar se há referência à proacl no código ou comentários
rg "proacl|acldefault|aclexplode" --type sql supabase/migrations/ -n

Repository: adm01-debug/Promo_Gifts

Length of output: 521


🌐 Web query:

PostgreSQL aclexplode NULL behavior proacl functions default permissions

💡 Result:

In PostgreSQL, the aclexplode function is a system function used to decompose an aclitem array (such as those found in system catalog columns like relacl) into a set of rows, where each row represents a specific privilege grant [1][2]. Regarding NULL behavior and default permissions: 1. NULL in ACL Columns: When an ACL column (like relacl in pg_class) is NULL, it signifies that the object is using the system's hard-wired default privileges [3][4]. It does not mean that no privileges exist; rather, it indicates that the default access rules apply [3]. 2. aclexplode and NULLs: The aclexplode function does not automatically resolve NULL values into the hard-wired default privileges [5]. If you pass a NULL value to aclexplode, it will return no rows, as there are no explicit grants to explode [1][5]. 3. Default Privileges: To determine the effective privileges for an object, you must account for both explicit grants (stored in the ACL column) and the implicit hard-wired defaults [3][6]. Because aclexplode only processes the explicit ACL array, it is not sufficient on its own to reconstruct the full set of effective permissions if the ACL column is NULL [5]. 4. Handling NULLs: If you need to programmatically determine effective permissions, you must implement logic to check if the ACL column is NULL and, if so, apply the corresponding hard-wired default privileges for that object type [5][3]. The system catalog pg_default_acl is used for user-defined default privileges (set via ALTER DEFAULT PRIVILEGES), but it is only consulted during object creation and does not replace the hard-wired defaults for existing objects [3][7][6]. In summary, aclexplode only explodes explicit grants [1][8]. A NULL value in an ACL column represents the presence of hard-wired default privileges, which aclexplode does not expand [5][3].

Citations:


🏁 Script executed:

# Verificar se proowner está disponível e check lógica proposta
rg "proowner" supabase/migrations/ -A 2 -B 2

Repository: adm01-debug/Promo_Gifts

Length of output: 962


🏁 Script executed:

# Procurar por CREATE FUNCTION sem GRANT explícito nas migrations
rg "CREATE.*FUNCTION.*public\." supabase/migrations/ | head -20

Repository: adm01-debug/Promo_Gifts

Length of output: 2996


🏁 Script executed:

# Verificar funções SECURITY DEFINER públicas neste repo
rg "CREATE.*FUNCTION.*public\." supabase/migrations/ -A 5 | grep -E "CREATE|SECURITY DEFINER" | head -40

Repository: adm01-debug/Promo_Gifts

Length of output: 4747


🏁 Script executed:

# Buscar especificamente por funções SECURITY DEFINER que não têm GRANT explícito logo após
rg "SECURITY DEFINER" supabase/migrations/ -B 2 -A 8 | head -80

Repository: adm01-debug/Promo_Gifts

Length of output: 7464


Gate de auditoria ACL deixa falso negativo: proacl IS NULL não é expandido para permissões efetivas default.

O gate audit_security_definer_acl() usa aclexplode(d.proacl) em acl_expanded, mas quando proacl IS NULL (ACL hard-wired default, não explícita), aclexplode não retorna linhas. Funções SECURITY DEFINER sem GRANT explícito têm proacl=NULL por padrão e herdam permissões efetivas via acldefault()—casos que o gate não detecta.

Risco crítico: Funções com PUBLIC EXECUTE efetivo podem passar pelo gate sem alerta, replicando o problema que a migration tenta corrigir.

Patch objetivo
  WITH defs AS (
    SELECT
      p.oid,
      p.proname,
      pg_get_function_identity_arguments(p.oid) AS args,
+     p.proowner,
      p.proacl,
      (pg_get_function_result(p.oid) = 'trigger') AS is_trigger,
      (p.proname IN ('submit_quote_response', 'get_quote_token_by_value')) AS public_intent
    FROM pg_proc p
    JOIN pg_namespace n ON n.oid = p.pronamespace
    WHERE n.nspname = 'public' AND p.prosecdef = true
  ),
  acl_expanded AS (
    SELECT
      d.oid,
      d.proname,
      d.args,
      d.is_trigger,
      d.public_intent,
      a.grantee::regrole::text AS grantee
    FROM defs d
    LEFT JOIN LATERAL (
-     SELECT (aclexplode(d.proacl)).grantee
+     SELECT (aclexplode(COALESCE(d.proacl, acldefault('f', d.proowner)))).grantee
    ) a ON true
    WHERE a.grantee IS NOT NULL
  ),

Também afeta Caso 4 (linhas 128–137): funções em policies sem EXECUTE pra authenticated não serão detectadas se usarem ACL default.

🤖 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/20260515150000_onda20_fix_t38_regression_and_bilateral_gate.sql`
around lines 83 - 96, O CTE acl_expanded usa aclexplode(d.proacl) e ignora
objetos com proacl IS NULL (ACL implícita), por isso a função
audit_security_definer_acl() não detecta permissões efetivas vindas de
acldefault; corrija substituindo/estendendo a expansão para também usar
acldefault quando proacl IS NULL (por exemplo aclexplode(coalesce(d.proacl,
acldefault(...))) ou fazendo um UNION LATERAL com aclexplode(acldefault(...)))
garantindo que a lógica que verifica grantee/public_intent em acl_expanded (e o
mesmo padrão aplicado ao caso 4) passe a considerar as permissões default além
das ACLs explícitas.

-- funções SECURITY DEFINER citadas em pg_policy (qual ou with_check)
-- regex \m...\M = word boundary, evita falso positivo (is_admin vs is_admin_or_above)
policy_uses AS (
SELECT DISTINCT d.oid, d.proname, d.args
FROM defs d
JOIN pg_policies pp
ON pp.schemaname = 'public'
AND (
COALESCE(pp.qual, '') ~ ('\m' || d.proname || '\M')
OR COALESCE(pp.with_check, '') ~ ('\m' || d.proname || '\M')
)
)
-- Caso 1: PUBLIC EXECUTE (sempre proibido)
SELECT proname, args, 'PUBLIC has EXECUTE'::text, 'PUBLIC'::text
FROM acl_expanded
WHERE grantee = '-'
UNION ALL
-- Caso 2: anon EXECUTE fora da whitelist
SELECT proname, args, 'anon has EXECUTE (not in public-intent whitelist)'::text, 'anon'
FROM acl_expanded
WHERE grantee = 'anon' AND NOT public_intent
UNION ALL
-- Caso 3: trigger function com EXECUTE pra authenticated (sem sentido)
SELECT proname, args, 'trigger function has EXECUTE for authenticated'::text, 'authenticated'
FROM acl_expanded
WHERE grantee = 'authenticated' AND is_trigger
UNION ALL
-- Caso 4 (NOVO — Onda 20): usada em policy mas authenticated sem EXECUTE
-- Caller authenticated falha com `42501: permission denied for function`
-- ao avaliar a RLS policy. Detecta o anti-padrão que quebrou prod
-- via PR #192 (t38).
SELECT
pu.proname,
pu.args,
'used in RLS policy but missing EXECUTE for authenticated (RLS will fail with 42501)'::text,
'authenticated (MISSING)'::text
FROM policy_uses pu
WHERE NOT EXISTS (
SELECT 1 FROM acl_expanded a
WHERE a.oid = pu.oid AND a.grantee = 'authenticated'
)
ORDER BY 1, 2;
$$;

COMMENT ON FUNCTION public.audit_security_definer_acl() IS
'Audit gate bilateral: detecta 4 anti-padrões em funções SECURITY DEFINER de public. (1) PUBLIC EXECUTE; (2) anon EXECUTE fora da whitelist (submit_quote_response, get_quote_token_by_value); (3) trigger function com EXECUTE pra authenticated; (4) função citada em pg_policy SEM EXECUTE pra authenticated (RLS quebra em runtime com 42501 — Caso adicionado em Onda 20 após PR #192/t38 regression). Lints Supabase 0028/0029. Usado por scripts/check-security-definer-acl.mjs. Ver docs/SECURITY-DEFINER-PATTERN.md.';

GRANT EXECUTE ON FUNCTION public.audit_security_definer_acl() TO authenticated, service_role;

-- ─────────────────────────────────────────────────────────────────
-- 4. Fail-fast: rollback se sobrar violação após cirurgia
-- ─────────────────────────────────────────────────────────────────
DO $$
DECLARE
v_count int;
v_violations text;
BEGIN
SELECT count(*) INTO v_count FROM public.audit_security_definer_acl();

IF v_count > 0 THEN
SELECT string_agg(
format(' - %s(%s) | %s [%s]', function_name, arguments, problem, granted_to),
E'\n'
) INTO v_violations
FROM public.audit_security_definer_acl();

RAISE EXCEPTION E'Onda 20: % violação(ões) SECURITY DEFINER restantes após cirurgia:\n%\nVer docs/SECURITY-DEFINER-PATTERN.md',
v_count, v_violations;
END IF;

RAISE NOTICE 'Onda 20: SECURITY DEFINER ACL gate limpo (0 violações).';
END $$;
Loading