@luanpdd/kit-mcp 1.22.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -1
- package/kit/agents/audit-log-implementer.md +138 -0
- package/kit/agents/auditor-consistencia-isolamento.md +33 -0
- package/kit/agents/crm-pipeline-implementer.md +89 -0
- package/kit/agents/debugger.md +41 -0
- package/kit/agents/evolution-go-integrator.md +21 -0
- package/kit/agents/executor.md +41 -0
- package/kit/agents/invite-flow-implementer.md +52 -0
- package/kit/agents/lgpd-compliance-auditor.md +89 -0
- package/kit/agents/multi-tenant-rls-writer.md +78 -0
- package/kit/agents/org-onboarding-implementer.md +21 -0
- package/kit/agents/planner.md +31 -0
- package/kit/agents/supabase-architect.md +17 -0
- package/kit/agents/supabase-auth-bootstrapper.md +80 -0
- package/kit/agents/supabase-column-privileges-writer.md +399 -0
- package/kit/agents/supabase-migration-writer.md +129 -14
- package/kit/agents/supabase-rbac-implementer.md +392 -0
- package/kit/agents/supabase-rls-hardener.md +521 -0
- package/kit/agents/supabase-rls-writer.md +105 -9
- package/kit/agents/supabase-roles-implementer.md +355 -0
- package/kit/agents/super-admin-implementer.md +99 -0
- package/kit/commands/supabase.md +55 -8
- package/kit/file-manifest.json +32 -24
- package/kit/skills/_shared-supabase/glossary.md +27 -0
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +37 -0
- package/kit/skills/supabase-column-level-security/SKILL.md +426 -0
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -0
- package/kit/skills/supabase-database-functions/SKILL.md +85 -0
- package/kit/skills/supabase-migrations/SKILL.md +123 -11
- package/kit/skills/supabase-postgres-roles/SKILL.md +392 -0
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -0
- package/kit/skills/supabase-rls-policies/SKILL.md +462 -12
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supabase-custom-claims-rbac
|
|
3
|
+
description: Use ao implementar Custom Claims via Custom Access Token Auth Hook para RBAC em Supabase — pattern canônico 7 passos (enum types + user_roles + role_permissions + auth hook + supabase_auth_admin grants + authorize() function + RLS policies + client jwt-decode). Substitui helper functions PG STABLE com JOIN custoso. Caveat JWT freshness (eventually consistent). v1.25 incorpora 100% da doc oficial Supabase.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — Custom Claims & RBAC via Auth Hooks
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando implementar **Role-Based Access Control (RBAC)** via Custom Claims no JWT. Pattern canônico Supabase v1.25.
|
|
11
|
+
|
|
12
|
+
Trigger phrases:
|
|
13
|
+
|
|
14
|
+
- "custom claims Supabase", "RBAC Supabase"
|
|
15
|
+
- "Custom Access Token Auth Hook", "auth hook RBAC"
|
|
16
|
+
- "user_role no JWT", "authorize() function"
|
|
17
|
+
- "role-based access control com auth hook"
|
|
18
|
+
- "como evitar JOIN em RLS policy" (custom claims é a alternativa moderna)
|
|
19
|
+
|
|
20
|
+
## Princípio canônico
|
|
21
|
+
|
|
22
|
+
RBAC em Supabase tem **3 mecanismos de delivery** dos claims:
|
|
23
|
+
|
|
24
|
+
1. **`app_metadata`** (skill `supabase-rls-policies` v1.23) — JSONB no JWT setado via service_role admin API. Simples mas limitado (não suporta enum types, sem normalização).
|
|
25
|
+
2. **Dedicated role table com helper function STABLE** (skill `rbac-permissions-matrix-supabase` v1.21) — `user_roles` + `private.has_role(role)` consultada em policies. Dinâmico mas faz JOIN custoso por query.
|
|
26
|
+
3. **Custom Claims via Custom Access Token Auth Hook** (v1.25 — **RECOMENDADO**) — `user_role` injetado no JWT durante geração do token via auth hook. Evita JOIN em policies (claim lido direto via `auth.jwt() ->> 'user_role'`). Eventually consistent (refresh TTL).
|
|
27
|
+
|
|
28
|
+
**Quando usar custom claims (v1.25):**
|
|
29
|
+
|
|
30
|
+
- ✅ RBAC com 2-10 roles fixos por user (admin, moderator, user, etc.)
|
|
31
|
+
- ✅ Permission matrix relativamente estática (mudanças em horas/dias, não segundos)
|
|
32
|
+
- ✅ Policies que precisam ser rápidas (sem JOIN custoso)
|
|
33
|
+
- ✅ Cliente front-end precisa saber o role (UI conditional rendering)
|
|
34
|
+
|
|
35
|
+
**Quando NÃO usar custom claims:**
|
|
36
|
+
|
|
37
|
+
- ❌ Permission mudada em real-time (segundos) — JWT freshness não cobre
|
|
38
|
+
- ❌ Permission depende de row context (ex: "can edit if owner of this row") — use RLS row-level com `auth.uid()`
|
|
39
|
+
- ❌ Multi-tenant com user em N orgs com role diferente em cada — custom claim é per-user, não per-org-context
|
|
40
|
+
|
|
41
|
+
Para multi-tenant complexo, combine: custom claim para role global (super_admin) + RLS hierárquica com `private.has_role(role, org_id)` para context-aware (skill `multi-tenant-rls-hierarchy`).
|
|
42
|
+
|
|
43
|
+
## Pattern canônico — 7 passos
|
|
44
|
+
|
|
45
|
+
### Passo 1: Enum types
|
|
46
|
+
|
|
47
|
+
Defina enum types Postgres para roles e permissions — tipo-seguro, refactorable, documentado.
|
|
48
|
+
|
|
49
|
+
```sql
|
|
50
|
+
-- enum de roles canônicos
|
|
51
|
+
create type public.app_role as enum ('admin', 'moderator', 'user');
|
|
52
|
+
|
|
53
|
+
-- enum de permissions canônicos (resource.action)
|
|
54
|
+
create type public.app_permission as enum (
|
|
55
|
+
'channels.delete',
|
|
56
|
+
'channels.create',
|
|
57
|
+
'messages.delete',
|
|
58
|
+
'messages.update',
|
|
59
|
+
'users.ban'
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Caveat:** adicionar novo enum value exige migration `alter type public.app_permission add value 'novo.permission'` — não pode ser feito dentro de transação (Postgres limitation).
|
|
64
|
+
|
|
65
|
+
### Passo 2: Tabelas user_roles + role_permissions
|
|
66
|
+
|
|
67
|
+
```sql
|
|
68
|
+
-- USER → ROLE mapping (N:1 user pode ter múltiplos roles)
|
|
69
|
+
create table public.user_roles (
|
|
70
|
+
id bigint generated by default as identity primary key,
|
|
71
|
+
user_id uuid references auth.users on delete cascade not null,
|
|
72
|
+
role app_role not null,
|
|
73
|
+
unique (user_id, role)
|
|
74
|
+
);
|
|
75
|
+
comment on table public.user_roles is 'Application roles for each user.';
|
|
76
|
+
|
|
77
|
+
-- ROLE → PERMISSION mapping (N:N role tem múltiplas permissions)
|
|
78
|
+
create table public.role_permissions (
|
|
79
|
+
id bigint generated by default as identity primary key,
|
|
80
|
+
role app_role not null,
|
|
81
|
+
permission app_permission not null,
|
|
82
|
+
unique (role, permission)
|
|
83
|
+
);
|
|
84
|
+
comment on table public.role_permissions is 'Application permissions for each role.';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Seed inicial:
|
|
88
|
+
|
|
89
|
+
```sql
|
|
90
|
+
insert into public.role_permissions (role, permission)
|
|
91
|
+
values
|
|
92
|
+
('admin', 'channels.delete'),
|
|
93
|
+
('admin', 'channels.create'),
|
|
94
|
+
('admin', 'messages.delete'),
|
|
95
|
+
('admin', 'messages.update'),
|
|
96
|
+
('admin', 'users.ban'),
|
|
97
|
+
('moderator', 'messages.delete'),
|
|
98
|
+
('moderator', 'messages.update');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Passo 3: Custom Access Token Auth Hook function
|
|
102
|
+
|
|
103
|
+
A função `custom_access_token_hook(event jsonb) returns jsonb` roda **antes** do JWT ser issued — recebe o event (com `user_id` + claims atuais) e devolve event modificado.
|
|
104
|
+
|
|
105
|
+
```sql
|
|
106
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
107
|
+
returns jsonb
|
|
108
|
+
language plpgsql
|
|
109
|
+
stable
|
|
110
|
+
as $$
|
|
111
|
+
declare
|
|
112
|
+
claims jsonb;
|
|
113
|
+
user_role public.app_role;
|
|
114
|
+
begin
|
|
115
|
+
-- buscar role do user em user_roles
|
|
116
|
+
select role into user_role
|
|
117
|
+
from public.user_roles
|
|
118
|
+
where user_id = (event->>'user_id')::uuid;
|
|
119
|
+
|
|
120
|
+
claims := event->'claims';
|
|
121
|
+
|
|
122
|
+
if user_role is not null then
|
|
123
|
+
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
|
124
|
+
else
|
|
125
|
+
claims := jsonb_set(claims, '{user_role}', 'null');
|
|
126
|
+
end if;
|
|
127
|
+
|
|
128
|
+
-- atualiza objeto claims no event original
|
|
129
|
+
event := jsonb_set(event, '{claims}', claims);
|
|
130
|
+
|
|
131
|
+
return event;
|
|
132
|
+
end;
|
|
133
|
+
$$;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Caveat para multi-role:** se user tem múltiplos roles em `user_roles`, esta versão pega o primeiro (`select role into user_role`). Para múltiplos roles no JWT, use:
|
|
137
|
+
|
|
138
|
+
```sql
|
|
139
|
+
-- variante multi-role: claim user_roles como array
|
|
140
|
+
select array_agg(role) into user_roles_array
|
|
141
|
+
from public.user_roles
|
|
142
|
+
where user_id = (event->>'user_id')::uuid;
|
|
143
|
+
-- ...
|
|
144
|
+
claims := jsonb_set(claims, '{user_roles}', to_jsonb(user_roles_array));
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Passo 4: Permissions canônicos para `supabase_auth_admin`
|
|
148
|
+
|
|
149
|
+
`supabase_auth_admin` é o Postgres role usado pelo Supabase Auth service ao invocar o hook. Precisa de permissions específicos:
|
|
150
|
+
|
|
151
|
+
```sql
|
|
152
|
+
-- 1. acesso ao schema public
|
|
153
|
+
grant usage on schema public to supabase_auth_admin;
|
|
154
|
+
|
|
155
|
+
-- 2. permissão de EXECUTE no hook function (apenas auth_admin pode invocar)
|
|
156
|
+
grant execute
|
|
157
|
+
on function public.custom_access_token_hook
|
|
158
|
+
to supabase_auth_admin;
|
|
159
|
+
|
|
160
|
+
-- 3. REVOKE EXECUTE de roles públicos (cliente NÃO pode chamar diretamente)
|
|
161
|
+
revoke execute
|
|
162
|
+
on function public.custom_access_token_hook
|
|
163
|
+
from authenticated, anon, public;
|
|
164
|
+
|
|
165
|
+
-- 4. acesso à tabela user_roles
|
|
166
|
+
grant all
|
|
167
|
+
on table public.user_roles
|
|
168
|
+
to supabase_auth_admin;
|
|
169
|
+
|
|
170
|
+
-- 5. REVOKE acesso público à user_roles (cliente NÃO pode mutar roles)
|
|
171
|
+
revoke all
|
|
172
|
+
on table public.user_roles
|
|
173
|
+
from authenticated, anon, public;
|
|
174
|
+
|
|
175
|
+
-- 6. RLS policy permitindo supabase_auth_admin ler user_roles
|
|
176
|
+
create policy "Allow auth admin to read user roles" on public.user_roles
|
|
177
|
+
as permissive for select
|
|
178
|
+
to supabase_auth_admin
|
|
179
|
+
using (true);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Por quê REVOKE EXECUTE do hook:** sem isso, qualquer cliente autenticado poderia chamar `custom_access_token_hook(...)` e potencialmente abusar.
|
|
183
|
+
|
|
184
|
+
**Por quê REVOKE ALL FROM authenticated em user_roles:** roles são metadado privilegiado — apenas admins (service_role) podem inserir/atualizar. Cliente não deve ter SELECT direto na tabela (consultar via claim do JWT).
|
|
185
|
+
|
|
186
|
+
### Passo 5: Habilitar o hook
|
|
187
|
+
|
|
188
|
+
**Dashboard (production):**
|
|
189
|
+
|
|
190
|
+
1. Acesse `Authentication > Hooks (Beta)` no Dashboard Supabase
|
|
191
|
+
2. Selecione "Custom Access Token" hook type
|
|
192
|
+
3. Dropdown: selecione `public.custom_access_token_hook` (função criada no Passo 3)
|
|
193
|
+
4. Save
|
|
194
|
+
|
|
195
|
+
**Local development:**
|
|
196
|
+
|
|
197
|
+
Em `supabase/config.toml`:
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[auth.hook.custom_access_token]
|
|
201
|
+
enabled = true
|
|
202
|
+
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Reinicie o Supabase local (`supabase stop && supabase start`).
|
|
206
|
+
|
|
207
|
+
### Passo 6: `authorize()` function
|
|
208
|
+
|
|
209
|
+
A função `authorize(permission app_permission) returns boolean` é o coração do RBAC — lê `user_role` do JWT e checa se o role tem a permission.
|
|
210
|
+
|
|
211
|
+
```sql
|
|
212
|
+
create or replace function public.authorize(
|
|
213
|
+
requested_permission app_permission
|
|
214
|
+
)
|
|
215
|
+
returns boolean
|
|
216
|
+
language plpgsql
|
|
217
|
+
stable
|
|
218
|
+
security definer
|
|
219
|
+
set search_path = ''
|
|
220
|
+
as $$
|
|
221
|
+
declare
|
|
222
|
+
bind_permissions int;
|
|
223
|
+
user_role public.app_role;
|
|
224
|
+
begin
|
|
225
|
+
-- ler user_role do JWT (delivered via auth hook do Passo 3)
|
|
226
|
+
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
|
|
227
|
+
|
|
228
|
+
-- contar permissions matching
|
|
229
|
+
select count(*)
|
|
230
|
+
into bind_permissions
|
|
231
|
+
from public.role_permissions
|
|
232
|
+
where role_permissions.permission = requested_permission
|
|
233
|
+
and role_permissions.role = user_role;
|
|
234
|
+
|
|
235
|
+
return bind_permissions > 0;
|
|
236
|
+
end;
|
|
237
|
+
$$;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Decisões canônicas:**
|
|
241
|
+
|
|
242
|
+
- `stable` — função consulta DB mas não muta; resultado é estável dentro de uma transação (Postgres pode cachear)
|
|
243
|
+
- `security definer` — roda com privilégios do owner (geralmente `postgres`) — necessário para acessar role_permissions sem RLS recursivo
|
|
244
|
+
- `set search_path = ''` — anti schema injection (cross-ref skill `supabase-database-functions`)
|
|
245
|
+
|
|
246
|
+
### Passo 7: RLS policies usando authorize()
|
|
247
|
+
|
|
248
|
+
Em vez de hard-code role em policy, use `authorize(permission)`:
|
|
249
|
+
|
|
250
|
+
```sql
|
|
251
|
+
-- ANTES (anti-pattern): hard-code role direto na policy
|
|
252
|
+
create policy "Allow admin delete channels" on public.channels for delete
|
|
253
|
+
to authenticated
|
|
254
|
+
using ((auth.jwt() ->> 'user_role') = 'admin');
|
|
255
|
+
|
|
256
|
+
-- DEPOIS (canônico v1.25): authorize() abstrai role → permission
|
|
257
|
+
create policy "Allow authorized delete access" on public.channels for delete
|
|
258
|
+
to authenticated
|
|
259
|
+
using ((SELECT authorize('channels.delete')));
|
|
260
|
+
|
|
261
|
+
create policy "Allow authorized delete access" on public.messages for delete
|
|
262
|
+
to authenticated
|
|
263
|
+
using ((SELECT authorize('messages.delete')));
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Vantagens canônicas:**
|
|
267
|
+
|
|
268
|
+
- Adicionar nova permission = INSERT em role_permissions (sem alterar policy)
|
|
269
|
+
- Mudar quem tem permission = UPDATE em role_permissions
|
|
270
|
+
- Policies ficam estáveis; matriz permissions evolui
|
|
271
|
+
- Reutilizável em N policies
|
|
272
|
+
|
|
273
|
+
**Por que `(SELECT authorize(...))` com wrapper:** caching (cross-ref REGRA #2 da skill `supabase-rls-policies` v1.23) — função roda 1 vez por query, não 1 vez por linha.
|
|
274
|
+
|
|
275
|
+
## Client-side — acessar custom claims
|
|
276
|
+
|
|
277
|
+
Auth hook só modifica o **access_token JWT**, não a auth response. Para acessar custom claims no cliente, decode o JWT:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npm install jwt-decode
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { jwtDecode } from 'jwt-decode'
|
|
285
|
+
import { createClient } from '@supabase/supabase-js'
|
|
286
|
+
|
|
287
|
+
const supabase = createClient(URL, ANON_KEY)
|
|
288
|
+
|
|
289
|
+
// listener para mudanças de auth state (login, logout, refresh)
|
|
290
|
+
supabase.auth.onAuthStateChange(async (event, session) => {
|
|
291
|
+
if (session) {
|
|
292
|
+
const jwt = jwtDecode(session.access_token)
|
|
293
|
+
const userRole = jwt.user_role // 'admin' | 'moderator' | 'user' | null
|
|
294
|
+
|
|
295
|
+
// usar userRole para UI conditional rendering
|
|
296
|
+
if (userRole === 'admin') {
|
|
297
|
+
// show admin panel
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Para Next.js v16 + SSR:** decode o JWT no server-side via middleware ou Server Component:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
// app/middleware.ts ou Server Component
|
|
307
|
+
import { jwtDecode } from 'jwt-decode'
|
|
308
|
+
|
|
309
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
310
|
+
if (session) {
|
|
311
|
+
const jwt = jwtDecode(session.access_token)
|
|
312
|
+
const userRole = jwt.user_role
|
|
313
|
+
// ...
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Para backend (Node.js/Python/etc):** use bibliotecas equivalentes:
|
|
318
|
+
|
|
319
|
+
- Node.js: `jsonwebtoken`, `express-jwt`, `koa-jwt`
|
|
320
|
+
- Python: `PyJWT`
|
|
321
|
+
- Dart: `dart_jsonwebtoken`
|
|
322
|
+
- .NET: `Microsoft.AspNetCore.Authentication.JwtBearer`
|
|
323
|
+
|
|
324
|
+
## ⚠ Caveat — JWT Freshness
|
|
325
|
+
|
|
326
|
+
Mudanças em `user_roles` **NÃO** refletem imediatamente no JWT do user — apenas após **token refresh** (default TTL 1h em Supabase).
|
|
327
|
+
|
|
328
|
+
**Cenários:**
|
|
329
|
+
|
|
330
|
+
| Situação | Comportamento |
|
|
331
|
+
|----------|---------------|
|
|
332
|
+
| Admin adiciona role moderator ao user X | User X continua com `user_role: null` até próximo refresh (até 1h) |
|
|
333
|
+
| Admin remove role admin do user Y | User Y continua com `user_role: admin` no JWT atual; após refresh, policies bloqueiam |
|
|
334
|
+
| Permission adicionada em role_permissions (admin → users.ban) | Reflete IMEDIATAMENTE — authorize() consulta DB cada query (stable mas re-executa em query nova) |
|
|
335
|
+
|
|
336
|
+
**Implicações:**
|
|
337
|
+
|
|
338
|
+
- Adicionar role: aceitável demora (user pode logout/login para forçar refresh)
|
|
339
|
+
- **Remover role: PROBLEMA** — user comprometido pode usar JWT antigo até expirar. Para invalidação imediata:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
// force logout via admin API (server-side com service_role)
|
|
343
|
+
await supabase.auth.admin.signOut(userId)
|
|
344
|
+
// próximo request do user vai falhar; user precisa login novamente
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- Mudança em permissions matrix (role_permissions): IMEDIATA — não precisa refresh
|
|
348
|
+
|
|
349
|
+
## Anti-patterns
|
|
350
|
+
|
|
351
|
+
### Anti-pattern 1: Esquecer GRANT EXECUTE ao supabase_auth_admin
|
|
352
|
+
|
|
353
|
+
**Errado:**
|
|
354
|
+
```sql
|
|
355
|
+
create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ...;
|
|
356
|
+
-- esqueceu o GRANT EXECUTE TO supabase_auth_admin
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Por quê:** auth hook falha silenciosamente — JWT é issued mas SEM o claim `user_role`. Difícil de debug.
|
|
360
|
+
|
|
361
|
+
**Certo:** sempre incluir os 6 GRANTs/REVOKEs do Passo 4.
|
|
362
|
+
|
|
363
|
+
### Anti-pattern 2: Hardcode role em policy ao invés de authorize()
|
|
364
|
+
|
|
365
|
+
**Errado:**
|
|
366
|
+
```sql
|
|
367
|
+
create policy "admin_delete" on public.channels for delete to authenticated
|
|
368
|
+
using ((auth.jwt() ->> 'user_role') = 'admin');
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Por quê:** acoplamento policy ↔ role. Mudar quem pode deletar requer ALTER POLICY (DDL); com authorize(), basta UPDATE em role_permissions.
|
|
372
|
+
|
|
373
|
+
**Certo:** `using ((SELECT authorize('channels.delete')))` — policy estável, matriz evolui.
|
|
374
|
+
|
|
375
|
+
### Anti-pattern 3: Assumir JWT fresh sem invalidação
|
|
376
|
+
|
|
377
|
+
**Errado:**
|
|
378
|
+
```ts
|
|
379
|
+
// admin remove role admin do user X
|
|
380
|
+
await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
|
|
381
|
+
// CRÍTICO: user X ainda tem JWT antigo válido por até 1h
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Por quê:** revogação de role não é imediata. Em casos de comprometimento, atacante usa o JWT antigo para abusar de privilégios admin.
|
|
385
|
+
|
|
386
|
+
**Certo:** sempre force logout após mudança crítica de role:
|
|
387
|
+
```ts
|
|
388
|
+
await supabase.from('user_roles').delete().eq('user_id', xId).eq('role', 'admin')
|
|
389
|
+
await supabase.auth.admin.signOut(xId) // força refresh imediato
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Anti-pattern 4: Mutar app_metadata do cliente para mudar role
|
|
393
|
+
|
|
394
|
+
**Errado:**
|
|
395
|
+
```ts
|
|
396
|
+
// no cliente
|
|
397
|
+
await supabase.auth.updateUser({ data: { role: 'admin' } })
|
|
398
|
+
// user_metadata é editável pelo cliente — privilege escalation
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Por quê:** mesmo anti-pattern de `user_metadata` da skill `supabase-rls-policies` (REGRA #1). Pior — confunde "role via custom claim" com "role via user_metadata".
|
|
402
|
+
|
|
403
|
+
**Certo:** roles são mutados APENAS via INSERT em `public.user_roles` por backend (service_role) ou admin API. Cliente nunca toca em metadata de role.
|
|
404
|
+
|
|
405
|
+
### Anti-pattern 5: Auth hook que faz query custosa
|
|
406
|
+
|
|
407
|
+
**Errado:**
|
|
408
|
+
```sql
|
|
409
|
+
create or replace function public.custom_access_token_hook(event jsonb) returns jsonb ... as $$
|
|
410
|
+
begin
|
|
411
|
+
-- query custosa em cada token issuance
|
|
412
|
+
select * into user_data from public.users
|
|
413
|
+
join public.organizations o on u.org_id = o.id
|
|
414
|
+
join public.subscriptions s on o.id = s.org_id
|
|
415
|
+
where u.id = (event->>'user_id')::uuid;
|
|
416
|
+
-- ...
|
|
417
|
+
end;
|
|
418
|
+
$$;
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Por quê:** hook roda em **cada login + cada refresh**. Query lenta degrada latência de auth. Bom hook é < 10ms.
|
|
422
|
+
|
|
423
|
+
**Certo:** mantenha hook simples — query single table (user_roles), aggregate se necessário, sem JOINs. Se precisa de info complexa, materialize em coluna de user_roles (denormalize).
|
|
424
|
+
|
|
425
|
+
## Postgres Roles vs Custom Claims — distinção canônica (v1.26)
|
|
426
|
+
|
|
427
|
+
**Custom Claims** (v1.25) e **Postgres Roles** (v1.26) são conceitos **complementares**, não substitutos:
|
|
428
|
+
|
|
429
|
+
| | Custom Claims (v1.25) | Postgres Roles (v1.26) |
|
|
430
|
+
|---|---|---|
|
|
431
|
+
| Tipo de access | Application access (end-users) | System access (service accounts) |
|
|
432
|
+
| Identidade | JWT claim `user_role` | Postgres login |
|
|
433
|
+
| Quem usa | End-users via PostgREST | Cron jobs, BI tools, ETL, admin scripts |
|
|
434
|
+
| Granularidade | Per-permission via `authorize()` | Per schema/table/function/column |
|
|
435
|
+
| Dinamicidade | Eventually consistent (TTL refresh) | Estática (alterações via SQL DDL) |
|
|
436
|
+
| Exemplo | "User admin pode deletar messages" | "Cron job pode SELECT em todas tabelas para backup" |
|
|
437
|
+
|
|
438
|
+
**Combine quando ambos aplicam:**
|
|
439
|
+
|
|
440
|
+
- End-user com role `admin` (via custom claim) acessa endpoint que invoca função `private.org_analytics()` com SECURITY DEFINER (que tem GRANT EXECUTE apenas para Postgres role `analytics_reader`)
|
|
441
|
+
- Service account `metabase_reader` (Postgres role) acessa view `public.user_active_tasks` com `security_invoker=true` (que respeita RLS via auth.jwt() — mas auth.jwt() é null para Postgres role login, então policies precisam considerar service_role bypass)
|
|
442
|
+
|
|
443
|
+
**NÃO use Postgres roles para application access:**
|
|
444
|
+
|
|
445
|
+
- ❌ Criar role Postgres `app_admin` para gerenciar quem é admin no app
|
|
446
|
+
- ❌ Criar role Postgres por end-user
|
|
447
|
+
- ✅ Use RLS + Custom Claims via auth hook (pattern v1.25)
|
|
448
|
+
|
|
449
|
+
**NÃO use Custom Claims para system access:**
|
|
450
|
+
|
|
451
|
+
- ❌ Service account cron job autenticando via JWT customizado
|
|
452
|
+
- ❌ BI tool authenticando via auth hook
|
|
453
|
+
- ✅ Use Postgres role dedicado (pattern v1.26)
|
|
454
|
+
|
|
455
|
+
Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
|
|
456
|
+
|
|
457
|
+
## Cross-suite integration (v1.25)
|
|
458
|
+
|
|
459
|
+
Esta skill é base para o agent novo `supabase-rbac-implementer` (Phase 139) — recebe spec (roles + permissions matrix) via `Task()` e materializa setup completo. Pattern de handoff cooperativo herdado de v1.23/v1.24.
|
|
460
|
+
|
|
461
|
+
Para multi-tenant com role por org, **combine** custom claim (role global) + helper function PG (`private.has_role(role, org_id)` em RLS hierárquica — skill `multi-tenant-rls-hierarchy` v1.21). Custom claim sozinho não cobre context-aware multi-tenant.
|
|
462
|
+
|
|
463
|
+
## Ver também
|
|
464
|
+
|
|
465
|
+
- [supabase-rbac-implementer](../../agents/supabase-rbac-implementer.md) (v1.25) — canonical materializer
|
|
466
|
+
- [supabase-rls-policies](../supabase-rls-policies/SKILL.md) (v1.23) — section "RBAC via Custom Claims + authorize() function (v1.25)"
|
|
467
|
+
- [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) (v1.24) — Camada 9 (Auth Hooks - Custom Claims) v1.25
|
|
468
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — pattern SECURITY DEFINER + supabase_auth_admin grants
|
|
469
|
+
- [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — Next.js v16 + onAuthStateChange + jwt-decode integration
|
|
470
|
+
- [supabase-rls-hardener](../../agents/supabase-rls-hardener.md) (v1.23) — Detector 9 valida auth hook instalado (v1.25)
|
|
471
|
+
- [rbac-permissions-matrix-supabase](../rbac-permissions-matrix-supabase/SKILL.md) (v1.21) — alternativa via helper function STABLE (vs custom claim v1.25)
|
|
472
|
+
- [glossário compartilhado](../_shared-supabase/glossary.md) — termos custom claims, Custom Access Token Auth Hook, JWT user_role claim, authorize() function, supabase_auth_admin role, app_role enum, app_permission enum, jwt-decode client pattern
|
|
@@ -160,6 +160,91 @@ end;
|
|
|
160
160
|
$$;
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
### GRANT EXECUTE por role hierarchy (v1.26)
|
|
164
|
+
|
|
165
|
+
Quando você cria função PG que será chamada por múltiplos service accounts, use role hierarchy + GRANT EXECUTE para gerenciar permissions:
|
|
166
|
+
|
|
167
|
+
```sql
|
|
168
|
+
-- group role para read-only services
|
|
169
|
+
create role "readers_group" noinherit;
|
|
170
|
+
|
|
171
|
+
-- service accounts individuais inheritam de readers_group
|
|
172
|
+
create role "metabase_reader" with login password '<secret>';
|
|
173
|
+
grant readers_group to metabase_reader;
|
|
174
|
+
|
|
175
|
+
create role "analytics_reader" with login password '<secret>';
|
|
176
|
+
grant readers_group to analytics_reader;
|
|
177
|
+
|
|
178
|
+
-- conceder EXECUTE em função canônica para o group
|
|
179
|
+
grant execute on function public.get_org_metrics(uuid) to readers_group;
|
|
180
|
+
-- agora metabase_reader e analytics_reader podem executar via inheritance
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Pattern com schema privado:**
|
|
184
|
+
|
|
185
|
+
```sql
|
|
186
|
+
-- função sensitive em schema private (não exposta via API)
|
|
187
|
+
create function private.expensive_aggregation(org_id uuid)
|
|
188
|
+
returns table(metric text, value bigint)
|
|
189
|
+
language plpgsql security definer set search_path = ''
|
|
190
|
+
as $$ ... $$;
|
|
191
|
+
|
|
192
|
+
-- revoke default
|
|
193
|
+
revoke execute on function private.expensive_aggregation(uuid) from public;
|
|
194
|
+
|
|
195
|
+
-- conceder apenas para custom role
|
|
196
|
+
grant execute on function private.expensive_aggregation(uuid) to readers_group;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Cross-ref completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
|
|
200
|
+
|
|
201
|
+
### Pattern Custom Access Token Auth Hook (v1.25)
|
|
202
|
+
|
|
203
|
+
Functions invocadas como **Auth Hooks** (ex: Custom Access Token) precisam permissions específicos para `supabase_auth_admin` role + REVOKE de roles públicos. Pattern canônico:
|
|
204
|
+
|
|
205
|
+
```sql
|
|
206
|
+
-- 1. função hook (stable, language plpgsql, modifica claims do JWT)
|
|
207
|
+
create or replace function public.custom_access_token_hook(event jsonb)
|
|
208
|
+
returns jsonb
|
|
209
|
+
language plpgsql
|
|
210
|
+
stable
|
|
211
|
+
as $$
|
|
212
|
+
declare
|
|
213
|
+
claims jsonb;
|
|
214
|
+
user_role public.app_role;
|
|
215
|
+
begin
|
|
216
|
+
select role into user_role from public.user_roles
|
|
217
|
+
where user_id = (event->>'user_id')::uuid;
|
|
218
|
+
|
|
219
|
+
claims := event->'claims';
|
|
220
|
+
if user_role is not null then
|
|
221
|
+
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
|
222
|
+
else
|
|
223
|
+
claims := jsonb_set(claims, '{user_role}', 'null');
|
|
224
|
+
end if;
|
|
225
|
+
event := jsonb_set(event, '{claims}', claims);
|
|
226
|
+
return event;
|
|
227
|
+
end;
|
|
228
|
+
$$;
|
|
229
|
+
|
|
230
|
+
-- 2. permissions canônicos para supabase_auth_admin (6 GRANTs/REVOKEs)
|
|
231
|
+
grant usage on schema public to supabase_auth_admin;
|
|
232
|
+
grant execute on function public.custom_access_token_hook to supabase_auth_admin;
|
|
233
|
+
revoke execute on function public.custom_access_token_hook from authenticated, anon, public;
|
|
234
|
+
grant all on table public.user_roles to supabase_auth_admin;
|
|
235
|
+
revoke all on table public.user_roles from authenticated, anon, public;
|
|
236
|
+
|
|
237
|
+
create policy "Allow auth admin to read user roles" on public.user_roles
|
|
238
|
+
as permissive for select to supabase_auth_admin using (true);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Decisões canônicas:**
|
|
242
|
+
- `stable` (não `volatile`) — hook não modifica DB, apenas lê user_roles
|
|
243
|
+
- **NÃO** usa `security definer` — hook roda com privilégios do `supabase_auth_admin` (que é o caller); GRANT EXECUTE necessário
|
|
244
|
+
- **REVOKE FROM authenticated/anon/public** — sem isso, qualquer cliente pode chamar o hook diretamente (abuse)
|
|
245
|
+
|
|
246
|
+
Padrão completo (RBAC end-to-end) em [`supabase-custom-claims-rbac`](../supabase-custom-claims-rbac/SKILL.md) (v1.25).
|
|
247
|
+
|
|
163
248
|
## Anti-patterns
|
|
164
249
|
|
|
165
250
|
### Anti-pattern 1: `SECURITY DEFINER` + sem `set search_path` + sem schema qualifier
|