Chapitre 5
Row-level security PostgreSQL : filet ou point d'application
Exploiter la row-level security sans qu'elle devienne une fuite : contexte posé par transaction, comportement derrière un pooler en mode transaction, rôles et propriétaires, politiques restrictives, plans mesurés sous le bon rôle, tests en SQL, puis le critère pour en faire un filet sous l'application ou le point d'application principal.
Par Elias Varen12 min de lectureDossier complet
À retenir
- Le tenant se pose dans chaque transaction par set_config avec un paramètre lié ; un réglage de session survit derrière un pooler en mode transaction et sert la requête d'un autre client.
- Une lecture en autocommit est sa propre transaction : sans transaction explicite, un réglage local disparaît avant la requête qu'il devait protéger.
- La politique de tenant s'écrit en RESTRICTIVE pour qu'aucune politique permissive ajoutée plus tard ne l'élargisse, et le rôle transverse reçoit une politique nommée plutôt que BYPASSRLS.
- Un plan ne se mesure que sous le rôle applicatif, sur le plus gros tenant ; un opérateur non LEAKPROOF peut perdre son index et faire lire tout le tenant.
- La RLS devient le point d'application principal seulement quand la base a des clients qui ne passent pas par vos repositories ; sinon elle reste un filet.
// Listener exécuté à l'ouverture de chaque connexion DBAL
public function onConnect(Connection $connection): void
{
$connection->executeQuery(
"SELECT set_config('app.tenant_id', ?, false)",
[$this->tenantContext->current()->toString()],
);
}Ce code fonctionne. Sous PHP-FPM sans connexions persistantes, chaque requête HTTP ouvre sa connexion, pose le tenant pour la session, exécute ses requêtes et ferme. Les politiques filtrent, les tests d'isolation passent, la production tourne. Le scénario qui suit est hypothétique et parfaitement ordinaire. Six mois plus tard, la plateforme compte quatre serveurs d'application à cinquante processus FPM chacun et trente workers, soit 230 connexions possibles vers une base qui commence à souffrir au-delà de cent. L'équipe place PgBouncer en mode transaction devant PostgreSQL. Le nombre de connexions serveur tombe à quarante, et le listener ci-dessus se met à servir à un client le tenant d'un autre.
L'article sur la row-level security pose les bases : politique USING et WITH CHECK, FORCE, objets qui passent au travers, test de catalogue. Je ne les reprends pas. Ce chapitre traite de ce qui arrive ensuite, quand la RLS doit tenir avec un pooler, plusieurs rôles, des workers, des sauvegardes, un plus gros client et une CI. Il se termine sur la décision que l'article tranchait vite, filet ou mécanisme principal.
Le contexte appartient à la transaction
Derrière un pooler en mode transaction, une connexion client ne correspond à une connexion serveur que le temps d'une transaction. Tout ce qui est posé au niveau de la session reste sur la connexion serveur après le COMMIT, et la transaction suivante, celle d'un autre processus PHP, en hérite. Le tenant de la session en fait partie, avec les verrous consultatifs de session, les LISTEN, les tables temporaires et, sur les versions anciennes du pooler, les prepared statements.
sequenceDiagram
participant A as "FPM 1 (tenant A)"
participant B as "FPM 2 (tenant B)"
participant P as "Pooler (transaction)"
participant S as "Connexion serveur S1"
A->>P: "set_config(tenant, A, false)"
P->>S: exécuté sur S1, la session de S1 garde A
P-->>A: S1 rendue au pool
B->>P: "SELECT … FROM invoice"
Note over B,P: "le set_config de B est parti sur S2"
P->>S: exécuté sur S1
S-->>B: lignes du tenant ALe paramètre doit donc être local à la transaction, et chaque unité de travail doit en être une. Ce second point casse plus souvent que le premier. En autocommit, chaque instruction forme sa propre transaction ; un set_config(…, true) envoyé seul est relâché à la fin de sa propre instruction, et le SELECT suivant part sans contexte. Avec une politique qui renvoie zéro ligne en l'absence de tenant, l'écran est vide. Avec la fonction bruyante décrite plus bas, il lève une erreur, ce que je préfère.