Data & IA

RLS PostgreSQL : tester qui peut lire et modifier quelles lignes

Deux rôles, quatre commandes et un vrai PostgreSQL : tester les lectures, les écritures refusées et le piège du propriétaire avec Row Level Security.

Miljan Stojiljkovic
31 Octobre 2026
9 min
nympharaidatapostgresqlrlsgouvernance

Deux équipes disposent du même droit SELECT sur une table de commandes. L’équipe France doit lire les commandes françaises, l’équipe Allemagne les allemandes. Un filtre dans le tableau de bord donne le bon affichage ; il ne démontre pas ce que la connexion SQL peut lire si ce filtre disparaît.

La Row Level Security de PostgreSQL, ou RLS, permet d’appliquer une politique aux lignes accessibles. Pour la vérifier, il faut exécuter la même requête sous plusieurs rôles, puis tenter les accès et les modifications qui doivent échouer.

Cet atelier utilise un vrai PostgreSQL 17.11 local, quatre commandes fictives et deux rôles SQL. Il montre le mécanisme et ses résultats, avec des fichiers reproductibles. Il ne valide ni l’authentification d’un SaaS ni l’isolation complète d’une application.

Une table traverse un contrôle d’accès vers les équipes FR et DE. Texte : PostgreSQL, contrôler l’accès aux lignes.

Le droit d’utiliser la table et le droit de voir une ligne

GRANT SELECT autorise la lecture de la table. GRANT INSERT et GRANT UPDATE autorisent les opérations correspondantes. Les droits sur le schéma restent nécessaires pour accéder à ses objets. Ces privilèges ne décrivent pas le territoire de chaque commande. Référence GRANT.

La politique RLS ajoute cette condition. Dans notre cas, les deux équipes ont les mêmes droits SQL ; le rôle actif détermine les lignes accessibles. Sans RLS, elles lisent toutes les deux les quatre commandes. Avec la politique, elles obtiennent deux ensembles distincts, même sans WHERE territoire = ... dans la requête.

Commande fictive Territoire Lecteur attendu
101 et 102 FR equipe_fr
201 et 202 DE equipe_de

Il faut conserver les deux niveaux de contrôle. Une politique qui accepte une ligne ne remplace pas un privilège SQL manquant : notre essai retire SELECT à l’équipe France et obtient un refus, malgré la politique.

Une politique lisible : rôle actif vers territoire

Le fichier schema.sql crée la table et trois rôles : son propriétaire, equipe_fr et equipe_de. Les équipes sont non propriétaires, sans SUPERUSER ni BYPASSRLS. Le propriétaire installe cette politique :

ALTER TABLE atelier.commandes ENABLE ROW LEVEL SECURITY;

CREATE POLICY territoire_equipe ON atelier.commandes
  FOR ALL TO equipe_fr, equipe_de
  USING (
    territoire = CASE current_user
      WHEN 'equipe_fr' THEN 'FR'
      WHEN 'equipe_de' THEN 'DE'
      ELSE NULL
    END
  )
  WITH CHECK (
    territoire = CASE current_user
      WHEN 'equipe_fr' THEN 'FR'
      WHEN 'equipe_de' THEN 'DE'
      ELSE NULL
    END
  );

USING examine les lignes existantes. Sous equipe_fr, une ligne allemande n’est pas visible. WITH CHECK examine l’état proposé par une insertion ou une modification : une commande française ne doit pas devenir allemande à l’initiative de ce rôle. Les deux expressions sont explicites pour séparer ces responsabilités. Syntaxe et application de CREATE POLICY.

L’identité est ici le rôle SQL current_user, associé à une valeur fixe. Ce choix rend l’expérience facile à examiner. Il ne propose pas de créer un rôle par utilisateur pour toutes les applications : la manière d’établir une identité fiable dépend de leur architecture.

Lancer l’atelier sur une base jetable

Prérequis : un terminal compatible sh, Docker installé et démarré, et l’image postgres:17-alpine disponible localement. Le client psql est exécuté dans le conteneur. Le script ne télécharge pas l’image ; vérifiez sa présence avec docker image inspect postgres:17-alpine. Si elle manque, installez-la depuis la distribution PostgreSQL de confiance utilisée par votre équipe avant de lancer l’exercice.

Téléchargez puis décompressez l’atelier RLS complet. Depuis le dossier décompressé, lancez :

sh ./run-local.sh ./journaux-rls

Le script crée un PostgreSQL isolé, sans port publié, sans réseau et sans dossier de l’ordinateur monté. Il installe les données, exécute les vérifications puis supprime son conteneur et ses volumes anonymes. Les journaux restent dans journaux-rls. Si un conteneur portant le même nom existe déjà, il s’arrête sans le modifier. Le README précise le nettoyage et la version de l’image.

L’exécution du 31 octobre 2026 a réussi 29 assertions sur PostgreSQL 17.11. Les résultats de référence donnent les rôles, identifiants et codes SQLSTATE observés. Ce nombre décrit les contrôles de cet exercice, pas un score de sécurité.

Pour contrôler votre exécution, code-sortie.txt doit contenir 0. Dans verifications.log, les 29 lignes NOTICE: RESULT doivent porter "status" : "pass". Un résultat différent arrête le script. Les refus attendus sont capturés et comparés : ils font partie d’un essai réussi.

Lire les résultats : le même SELECT sous deux rôles

Le banc d’essai part d’une connexion d’administration locale et choisit le rôle à examiner avec SET ROLE. Les contrôles de permissions des requêtes suivantes s’effectuent comme ce rôle. Les deux équipes sont NOLOGIN : cette expérience ne leur crée pas de connexion utilisateur autonome. Fonctionnement de SET ROLE.

Dans cette connexion de test, la séquence représentative est :

SET ROLE equipe_fr;
SELECT id FROM atelier.commandes ORDER BY id;
-- 101, 102
RESET ROLE;

SET ROLE equipe_de;
SELECT id FROM atelier.commandes ORDER BY id;
-- 201, 202
RESET ROLE;

Le fichier verifications.sql compare ces identifiants exacts, pas seulement le nombre de lignes. Deux commandes visibles pourraient être les mauvaises. Il vérifie aussi qu’une demande explicite de la commande 201 par l’équipe France renvoie un ensemble vide.

Le scénario commence par désactiver temporairement RLS : chaque équipe voit alors les quatre identifiants. Il active ensuite RLS sans politique applicable : l’équipe France ne voit aucune ligne. Ces modifications sont annulées avant l’essai de la politique. Elles rendent observable la différence entre privilège, activation et règle applicable. Le comportement de refus par défaut est décrit dans le guide de sécurité par ligne.

Une ligne cachée et une écriture interdite ne donnent pas le même résultat

Lire correctement deux lignes ne suffit pas. Le rôle France dispose aussi de INSERT et UPDATE ; sa politique doit rester cohérente après ces opérations.

Action sous equipe_fr Résultat observé
Insérer la commande 103 en FR Insertion acceptée, 103 visible
Insérer la commande 203 en DE Refus SQLSTATE 42501, aucune 203 créée
Changer le territoire de 101 de FR vers DE Refus 42501, 101 reste française
Modifier le libellé de 201, déjà cachée Aucune ligne modifiée, libellé inchangé
Modifier le libellé de 101 Modification acceptée

La distinction est utile pour lire les journaux d’une application. Une ligne exclue de la sélection peut produire zéro ligne modifiée ; une nouvelle valeur qui viole la politique peut produire une erreur. Une suite de tests qui attend « une erreur partout » rejetterait donc un comportement normal.

Les assertions contrôlent l’état après les tentatives refusées. Elles vérifient notamment que la commande allemande n’a pas changé. Les écritures d’essai sont ensuite annulées. La distinction entre lignes sélectionnables et nouvelles valeurs contrôlées vient de CREATE POLICY ; les sorties du tableau viennent de l’exécution fournie.

Le piège : vérifier avec le propriétaire de la table

Un développeur ouvre son client SQL avec le propriétaire, lance le SELECT et voit les quatre commandes. Ce résultat ne prouve pas que la politique est cassée : le propriétaire échappe normalement à RLS. Notre essai reproduit ce cas.

ALTER TABLE ... FORCE ROW LEVEL SECURITY soumet également le propriétaire aux politiques lorsqu’il consulte la table. Ici, il voit alors zéro ligne, car aucune politique ne s’applique à son rôle. Ce n’est pas le résultat attendu pour les équipes : c’est un contrôle de l’identité utilisée pour tester. Le propriétaire conserve le pouvoir de modifier la configuration. Options RLS d’ALTER TABLE.

Un superutilisateur ou un rôle BYPASSRLS continue de voir les quatre commandes, même avec FORCE. Ces deux sorties sont elles aussi vérifiées. Le rôle utilisé par une application doit donc être examiné autant que le texte de sa politique. Exceptions aux politiques RLS.

Avant d’adapter ce modèle à votre application

Deux points de conception changent directement la conclusion du test.

D’abord, la connexion applicative ne doit pas pouvoir choisir librement l’autre équipe. L’administrateur local peut sélectionner les deux rôles pour le banc d’essai ; ce pouvoir ne doit pas être donné aux utilisateurs de l’application. Si votre application partage un rôle SQL entre plusieurs clients, cet exemple n’a pas encore établi comment elle authentifie et transmet leur identité.

Ensuite, inventoriez toutes les politiques applicables. L’atelier ajoute une seconde politique permissive qui autorise 201 à l’équipe France : la sortie devient [101, 102, 201]. Une politique restrictive qui exclut ensuite 102 la réduit à [101, 201]. Ajouter une règle ne réduit donc pas toujours l’accès ; l’essai rend visibles la combinaison OR des permissives et l’effet AND de la restrictive. Combinaison des politiques.

Le script positionne aussi un paramètre libre app.territoire à DE sous equipe_fr. Les commandes visibles restent 101 et 102, car notre politique utilise le rôle SQL. Faire confiance à un paramètre modifiable exigerait un autre modèle de confiance. Le passer en SET LOCAL limite sa durée, sans authentifier sa valeur. Paramètres personnalisés et portée de SET.

Pour une première revue de votre propre table, écrivez une matrice rôle/opération/identifiants attendus, séparez le propriétaire des rôles applicatifs, puis testez les demandes de lignes étrangères et les changements de rattachement. Conservez les états après refus, pas uniquement les messages d’erreur.

L’atelier n’examine pas les vues, les fonctions privilégiées, les pools de connexions, les sauvegardes ou la performance. RLS ne couvre pas non plus toutes les opérations sur une table, notamment TRUNCATE. La décision obtenue ici est plus précise : vous disposez d’un test reproductible de votre politique sur des lectures et écritures définies, sous des rôles identifiés. Périmètre du guide PostgreSQL.

Pour prolonger les contrôles de votre chaîne data : tester les transformations SQL en CI.

Ressource gratuite

Checklist Audit IA 90 jours pour PME

Cadrage des process, données, cas d'usage, garde-fous RGPD/AI Act, quick wins, roadmap : 6 blocs et 28 points de contrôle, utilisables en autonomie. Reçue par email, sans séquence commerciale derrière.

Recevoir la checklist

Appliquer cette méthode à vos process

Votre équipe Data & IA externalisée, de 2 à 5 jours par semaine. L'atelier de cadrage inclus produit votre feuille de route priorisée par impact.