Contexte
Lors du smoke test post-PR #191 (fix 503 reste-à-vivre), l'insert audit_log pour l'event workspace.reste_a_vivre_updated a échoué avec permission denied for table audit_log. Bug isolé après la cause racine du 503 (workspace_members.order column rename).
Non-bloquant : logAuditEvent() est fire-and-forget avec catch (src/lib/security/audit-log.ts:126-129). L'app fonctionne, seul l'audit trail est cassé.
Hors scope de la PR security hardening (function grants + search_path) — déféré ici pour traitement dédié.
Trois hypothèses cause
H1 — Grant insert manquant pour service_role
Probabilité : faible — La table audit_log n'a aucun revoke explicite sur service_role ou postgres (vérifié dans toutes les migrations). service_role est owner-équivalent sur Supabase.
À vérifier (psql via Supabase MCP) :
```sql
select grantee, privilege_type, is_grantable
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'audit_log'
order by grantee, privilege_type;
```
H2 — RLS policy insert bloque
Probabilité : faible — service_role a bypassrls par défaut sur Supabase. Les policies audit_log_no_insert (migration 20260417000003) ciblent to anon, authenticated, pas service_role.
À vérifier :
```sql
select rolname, rolbypassrls from pg_roles where rolname = 'service_role';
select * from pg_policies where tablename = 'audit_log';
```
H3 — `createAdminClient()` mauvais client (PISTE FORTE)
Probabilité : forte. Vérifié dans `src/lib/supabase/server.ts:37-54` :
```ts
export async function createAdminClient() {
const cookieStore = await cookies();
return createServerClient(env.NEXT_PUBLIC_SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
cookies: {
getAll() { return cookieStore.getAll(); }, // Lit les cookies user
setAll() { /* noop */ },
},
auth: { autoRefreshToken: false, persistSession: false },
});
}
```
Problème : `createServerClient` de `@supabase/ssr` est conçu pour anon key + cookies user. Combiner `SERVICE_ROLE_KEY` + cookie handler qui lit les cookies user peut produire un comportement ambigu — selon la version du SDK, l'`Authorization` header peut être substitué par le JWT user présent en cookie, ce qui dégrade le client en rôle `authenticated`. Or les policies deny anon/authenticated bloquent alors l'insert.
Pattern canonique service_role = `createClient` de `@supabase/supabase-js` direct (sans SSR, sans cookies) :
```ts
import { createClient } from '@supabase/supabase-js';
export function createServiceRoleClient() {
return createClient(
env.NEXT_PUBLIC_SUPABASE_URL,
env.SUPABASE_SERVICE_ROLE_KEY,
{ auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false } },
);
}
```
Procédure d'investigation recommandée
-
Confirmer H3 : ajouter un log temporaire dans `logAuditEvent` pour capturer `current_user` et `current_setting('request.jwt.claims', true)` avant l'insert (wrapper RPC `whoami_audit_probe` ou query directe).
-
Si H3 confirmée : créer `createServiceRoleClient` direct, migrer les call-sites de `createAdminClient` un par un en validant :
- `src/lib/security/audit-log.ts` (logAuditEvent)
- `src/lib/auth/require-admin.ts` (admin RBAC check)
- `src/lib/gdpr/deletion.ts`
- `src/lib/gdpr/export.ts`
- `src/lib/gdpr/consent.ts`
-
Tests obligatoires :
- Re-smoke reste-à-vivre → audit_log insert doit réussir (event `workspace.reste_a_vivre_updated`)
- GDPR export/deletion flow → ne pas casser
- Admin RBAC check → ne pas casser
Impact
- Audit trail incomplet depuis l'introduction de `logAuditEvent` (peut affecter conformité GDPR art. 5(2) accountability + art. 30 record of processing si étendu à d'autres events)
- Pas d'impact fonctionnel utilisateur (fire-and-forget)
Critères de done
Références
Contexte
Lors du smoke test post-PR #191 (fix 503 reste-à-vivre), l'insert audit_log pour l'event
workspace.reste_a_vivre_updateda échoué avecpermission denied for table audit_log. Bug isolé après la cause racine du 503 (workspace_members.order column rename).Non-bloquant :
logAuditEvent()est fire-and-forget avec catch (src/lib/security/audit-log.ts:126-129). L'app fonctionne, seul l'audit trail est cassé.Hors scope de la PR security hardening (function grants + search_path) — déféré ici pour traitement dédié.
Trois hypothèses cause
H1 — Grant insert manquant pour service_role
Probabilité : faible — La table
audit_logn'a aucunrevokeexplicite surservice_roleoupostgres(vérifié dans toutes les migrations).service_roleest owner-équivalent sur Supabase.À vérifier (psql via Supabase MCP) :
```sql
select grantee, privilege_type, is_grantable
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'audit_log'
order by grantee, privilege_type;
```
H2 — RLS policy insert bloque
Probabilité : faible —
service_roleabypassrlspar défaut sur Supabase. Les policiesaudit_log_no_insert(migration20260417000003) ciblentto anon, authenticated, passervice_role.À vérifier :
```sql
select rolname, rolbypassrls from pg_roles where rolname = 'service_role';
select * from pg_policies where tablename = 'audit_log';
```
H3 — `createAdminClient()` mauvais client (PISTE FORTE)
Probabilité : forte. Vérifié dans `src/lib/supabase/server.ts:37-54` :
```ts
export async function createAdminClient() {
const cookieStore = await cookies();
return createServerClient(env.NEXT_PUBLIC_SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
cookies: {
getAll() { return cookieStore.getAll(); }, // Lit les cookies user
setAll() { /* noop */ },
},
auth: { autoRefreshToken: false, persistSession: false },
});
}
```
Problème : `createServerClient` de `@supabase/ssr` est conçu pour anon key + cookies user. Combiner `SERVICE_ROLE_KEY` + cookie handler qui lit les cookies user peut produire un comportement ambigu — selon la version du SDK, l'`Authorization` header peut être substitué par le JWT user présent en cookie, ce qui dégrade le client en rôle `authenticated`. Or les policies deny anon/authenticated bloquent alors l'insert.
Pattern canonique service_role = `createClient` de `@supabase/supabase-js` direct (sans SSR, sans cookies) :
```ts
import { createClient } from '@supabase/supabase-js';
export function createServiceRoleClient() {
return createClient(
env.NEXT_PUBLIC_SUPABASE_URL,
env.SUPABASE_SERVICE_ROLE_KEY,
{ auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false } },
);
}
```
Procédure d'investigation recommandée
Confirmer H3 : ajouter un log temporaire dans `logAuditEvent` pour capturer `current_user` et `current_setting('request.jwt.claims', true)` avant l'insert (wrapper RPC `whoami_audit_probe` ou query directe).
Si H3 confirmée : créer `createServiceRoleClient` direct, migrer les call-sites de `createAdminClient` un par un en validant :
Tests obligatoires :
Impact
Critères de done
Références