Ce qui est branché · 4 min
Base de données
Quatre tables, Row Level Security sur toutes, et comment prouver que les policies tiennent au lieu de l'espérer.
Le schéma tient dans un fichier, supabase/schema.sql, appliqué avec
npm run db:push. Il est écrit pour être rejoué : chaque create est en
if not exists et chaque policy est supprimée avant d'être créée.
Les tables
| Table | Contient | Écrite par |
|---|---|---|
profiles | Tout ce qui n'est pas de l'authentification | Un trigger, à l'inscription |
plans | La grille tarifaire | Vous, dans le seed en bas du schéma |
orders | Une ligne par paiement abouti | Le webhook Stripe uniquement |
subscriptions | L'état courant d'un abonnement | Le webhook Stripe uniquement |
Interdire par défaut
Row Level Security est activé sur chaque table, et les seules policies sont celles du schéma. Cela inverse le mode de défaillance habituel. Oubliez une policy et la requête ne renvoie rien, ce que vous remarquez en dix secondes. Oubliez un contrôle d'autorisation dans le code et la requête renvoie tout, ce que vous remarquez quand quelqu'un vous le signale.
alter table public.orders enable row level security;
create policy "orders are self readable"
on public.orders for select using (auth.uid() = user_id);Notez ce qui est absent : il n'y a pas de policy d'insertion sur orders. Un
utilisateur ne peut pas créer sa propre commande au prix qu'il veut depuis une
console de navigateur, parce que la seule chose qui écrit des commandes est le
webhook, avec la clé de service, qui contourne RLS par construction.
La récursion des policies
Dès que vous ajouterez des équipes, vous écrirez ceci :
create policy "members read the org" on organisations for select
using (exists (select 1 from memberships where org_id = id and user_id = auth.uid()));
create policy "members read memberships" on memberships for select
using (exists (select 1 from organisations where id = org_id));Postgres refusera les deux avec une erreur de récursion infinie, parce que
chaque policy interroge une table dont la policy interroge la première. La
sortie est une fonction security definer, qui s'exécute avec les droits du
propriétaire et ne rentre donc pas à nouveau dans la policy :
create or replace function public.is_member(target uuid)
returns boolean language sql security definer stable
set search_path = public as $$
select exists (
select 1 from memberships
where org_id = target and user_id = auth.uid()
);
$$;Les deux policies appellent ensuite is_member(...). Fixez toujours le
search_path sur une fonction security definer : sans lui, quelqu'un qui peut
créer un schéma peut masquer un nom de table et votre fonction s'exécute contre
la sienne.
Le prouver
Une policy que vous n'avez pas testée est une policy en laquelle vous croyez. Lisez avec la clé publiable, qui ne porte aucune session, et vérifiez que vous n'obtenez rien :
const res = await fetch(`${SUPABASE_URL}/rest/v1/orders?select=id`, {
headers: { apikey: PUBLISHABLE_KEY },
});
console.assert((await res.json()).length === 0, "les commandes fuient");Ce contrôle a sa place dans votre suite de tests, pas dans votre mémoire. Voir Tests.
Modifier le schéma
Éditez supabase/schema.sql, relancez npm run db:push. Pour une colonne qui
doit atterrir sur une base qui a déjà la table, utilisez la forme additive afin
que le fichier reste rejouable :
alter table public.profiles add column if not exists paypal_email text;Il n'y a pas de dossier de migrations ni d'historique. C'est un arbitrage assumé : un fichier lisible qui décrit toujours la forme actuelle, plutôt que quarante fichiers numérotés à rejouer de tête. Si vous le dépassez, les migrations du CLI Supabase cohabitent très bien.
Une erreur ou un manque sur cette page ? Dites-le-nous.