Devops Approach

Row Level Security sur Postgres, comment proteger l’accès aux données

Découvrez nos jobs
Vous ambitionnez de devenir Tech Lead ou de faire du conseil de haut-niveau ? Nous avons des challenges à votre hauteur !

Dans toute application se pose la question de la gestion des droits. La gestion des droits est généralement gérée par utilisateur, par groupe utilisateur ou par rôles ou profils. La gestion de qui peut lire, modifier ou supprimer les données est gérées dans le code, dans des requêtes SQL qui peuvent devenir très complexes : dans chaque accès à la base de données il faut penser à intégrer la gestion des droits et filtrer les données dans une requête where avec des jointures vers les tables de gestion des droits. Ceci n’est pas très performant, et peu poser des problèmes de sécurité si les requêtes sont mal écrites ou si un nouveau développeur oublie d’intégrer les filtres.

Depuis la version 9.5 de Postgres, une nouvelle feature permet de gérer tout cela directement dans la base de données ! On pouvait déjà définir des droits sur les tables (lecture, écriture, suppression sur toutes les données de la table) ou sur les colonnes (qui peut voir ou modifier certaines colonnes), et maintenant il est possible de gérer les droits par ligne dans une table. Cette nouveauté s’appelle Row Level Security.

Prenons un exemple : Sur un site de vente en ligne, un utilisateur ne doit voir que ses commandes dans le back office, et traditionnellement cela sera fait par une close where :

Nous avons créé une table orders et deux commandes pour les clients enzo et evan.
Quand Enzo se connecte au backoffice, on va afficher ses commandes avec la requête suivante :

L’idée des RLS est de créer des utilisateurs ou des rôles pour chaque client sur la base de données, et définir les droits d’accès selon l’utilisateur ou le rôle utilisé :

Ensuite nous allons activer RLS sur la table orders :

create table orders (order_id serial primary key, amount integer not null check
(amount > 0.0), time_placed timestamptz default current_timestamp, customer text);
insert into orders (amount, customer) values (10, 'enzo'), (20, 'evan');
select order_id, amount, customer from orders where customer = 'enzo';
create role enzo;
create role evan;
alter table orders enable row level security;
create policy orders_reader on orders for select using (customer = current_user);
grant select on orders to enzo;
grant select on orders to evan;

On active RLS sut la table orders, et ensuite on va mettre en place une règle : sur cette table, on ne va afficher que les lignes où le champ customer correspond à l’utilisateur ou le rôle connecté. Puis ensuite on autorise chaque utilisateur à accéder à la table.

set role enzo;
select order_id, amount, customer, time_placed from orders;
 order_id | amount | customer |         time_placed
 ----------+--------+----------+------------------------------
         1 |   10  | enzo     | 2015-08-21 14:05:13.03572-04

Et voilà ! L’utilisateur Enzo ne verra que ses données, sans devoir ajouter un filtre à chaque requête SQL. Idem pour Evan :

set role evan;
select order_id, amount, customer, time_placed from orders;
 order_id | amount | customer |         time_placed
 ----------+--------+----------+------------------------------
         2 |   20  | evan     | 2015-08-21 14:05:13.03572-04

Voyons un autre exemple plus complet : En effet, on ne stockera jamais le login de l’utilisateur dans la commande, on passera par une table customers :

create table customers (customer_id serial primary key, username text not null
unique);
insert into customers (username) values ('enzo'), ('evan');
create role enzo;
create role evan;
grant select on customers to enzo;
grant select on customers to evan;
create table orders (order_id serial primary key, amount integer not null check
(amount > 0.0), time_placed timestamptz default current_timestamp, column
customer_id integer references customers);
insert into orders (amount, customer) values (10, 1), (20, 2);

On crée une table customers et une table orders qui référence la table customers. On crée aussi les rôles pour nos clients et deux commandes.

Il faut maintenant filtrer la table orders de la façon suivante :

create policy orders_reader on orders for select using ( (select true from
customers where username = current_user and customers.customer_id =
orders.customer_id) );
set role enzo;
select * from orders;
 order_id | amount | customer |         time_placed

Et voilà, la table est filtrée selon une table liée, ce qui sera très souvent le cas en pratique.

On pourrait aussi appliquer une row policie sur la table customers pour limiter l’accès juste à la ligne de chaque utilisateur.

Désormais toute la logique de gestion de l’accès aux données est gérée par du paramétrage de la base de données, et pas dans chaque requête ou code d’accès.

Bien que cette fonctionnalité soit très pratique, elle présente tout de même certaines contraintes :

Au lieu d’avoir un seul utilisateur qui accède à la base de données, il faut gérer un compte ou rôle par client : On va définir le rôle à la création de la session. Il faut aussi créer ou supprimer un rôle de base de données quand on crée ou supprime un utilisateur de l’application.
Les Row Policies ne sont pas encore standardisées, il sera donc très compliqué de migrer la base de données vers une autre base qui ne gère pas cette feature.

En pratique nous avons eu des cas où lors d’un passage en recette ou en production les Row Policies n’était pas activées : Je conseille donc d’ajouter au monitoring de l’application un mécanisme qui vérifie que les règles sont bien activées.
Il faut absolument documenter toutes les règles mises en place, car rapidement il devient très compliqué de savoir quelle règle s’applique sur quelle table.

De plus l’impact sur les performances n’est pas négligeable, il faut créer les index qui vont bien pour que les row policies soient performantes.

Depuis quelques années les données personnelles deviennent des données critiques et très sensibles (RPGD, …), il convient donc de les sécuriser au maximum et dans ce cadre les RLS sont une très bonne solution.

----------+--------+----------+------------------------------
           | enzo     |

0