En plus des systèmes de droits du standard SQL disponibles via GRANT, les tables peuvent avoir des politiques de sécurité pour l'accès aux lignes qui restreignent, utilisateur par utilisateur, les lignes qui peuvent être renvoyées par les requêtes d'extraction ou les commandes d'insertions, de mises à jour ou de suppressions. Cette fonctionnalité est aussi connue sous le nom Row-Level Security. Par défaut, les tables n'ont aucune politique de ce type pour que, si un utilisateur a accès à une table selon les droits du standard SQL, toutes les lignes de la table soient accessibles aux requêtes de lecture ou d'écriture.
Lorsque la protection des lignes est activée sur une table (avec
l'instruction ALTER TABLE ... ENABLE
ROW LEVEL SECURITY), tous les accès classiques à la table
pour sélectionner ou modifier des lignes doivent être autorisés par
une politique de sécurité. Cependant, le propriétaire de la table
n'est typiquement pas soumis aux politiques de sécurité. Si aucune
politique n'existe pour la table, une politique de rejet est utilisée
par défaut, ce qui signifie qu'aucune ligne n'est visible ou
ne peut être modifiée. Les opérations qui s'appliquent pour
la table dans sa globalité, comme TRUNCATE
et REFERENCES
, ne sont pas soumises à ces
restrictions de niveau ligne.
Les politiques de sécurité niveau ligne peuvent s'appliquer en particulier
soit à des commandes, soit à des rôles, soit aux deux. Une politique
est indiquée comme s'appliquant à toutes les commandes par
ALL
, ou seulement à SELECT
,
INSERT
, UPDATE
ou
DELETE
. Plusieurs rôles peuvent être affectés
à une politique donnée, et les règles normales d'appartenance et
d'héritage s'appliquent.
Pour indiquer les lignes visibles ou modifiables pour une politique,
une expression renvoyant un booléen est
requise. Cette expression sera évaluée pour chaque ligne avant
toutes conditions ou fonctions qui seraient indiquées dans les
requêtes de l'utilisateur. (La seule exception à cette règle
est les fonctions marquées leakproof
, qui annoncent ne
pas dévoiler d'information ; l'optimiseur peut choisir d'appliquer
de telles fonctions avant les vérifications de sécurité niveau
ligne). Les lignes pour lesquelles l'expression ne renvoie pas
true
ne sont pas traitées. Des expressions
différentes peuvent être indiquées pour fournir des contrôles
indépendants pour les lignes qui sont visibles et pour celles qui sont
modifiées. Les expressions attachées à la politique sont exécutées
dans le cours de la requête et avec les droits de l'utilisateur
qui exécute la commande, bien que les fonctions définies avec
l'attribut SECURITY DEFINER
puissent être utilisées pour
accéder à des données qui ne seraient pas disponibles à l'utilisateur
effectuant la requête.
Les superutilisateurs et les rôles avec l'attribut
BYPASSRLS
ne sont pas soumis au système de
sécurité niveau ligne lorsqu'ils accèdent une table. Il en
est de même par défaut du propriétaire d'une table, bien
qu'il puisse choisir de se soumettre à ces contrôles avec
ALTER TABLE ... FORCE ROW LEVEL
SECURITY.
L'activation ou la désactivation de la sécurité niveau ligne, comme l'ajout des polices à une table, est toujours le privilège du seul propriétaire de la table.
Les politiques sont créées en utilisant l'instruction CREATE POLICY, modifiées avec la commande ALTER POLICY et supprimées avec la commande DROP POLICY. Pour activer et désactiver la sécurité niveau ligne pour une table donnée, utilisez la commande ALTER TABLE.
Chaque politique possède un nom et de multiples politiques peuvent être définies pour une table. Comme les politiques sont spécifiques à une table, chaque politique pour une même table doit avoir un nom différent. Différentes tables peuvent avoir des noms de politique de même nom.
Lorsque plusieurs politiques sont applicables pour une même requête,
elles sont combinées en utilisant OR
(pour les
politiques permissives, ce qui est le comportement par défaut) ou en
utilisant AND
(pour les politiques restrictives). C'est
similaire à la règle qu'un rôle donné a les privilèges de tous les rôles
dont il est membre. Les politiques permissives et restrictives sont
discutées plus en détail ci-dessous.
À titre de simple exemple, nous allons ici créer une politique sur
la relation comptes
pour autoriser seulement les
membres du rôle admins
à accéder seulement aux
lignes de leurs propres comptes :
CREATE TABLE comptes (admin text, societe text, contact_email text); ALTER TABLE comptes ENABLE ROW LEVEL SECURITY; CREATE POLICY compte_admins ON comptes TO admins USING (admin = current_user);
La politique ci-dessus prévoit implicitement une clause
WITH CHECK
identique à sa clause USING
,
de sorte que la contrainte s'applique à la fois aux lignes sélectionnées par
une commande (un gestionnaire ne peut donc pas utiliser
SELECT
, UPDATE
, ou DELETE
sur des lignes existantes appartenant à un gestionnaire différent) et aux
lignes modifiées par une commande (les lignes appartenant à un gestionnaire
différent ne peuvent donc être créées avec INSERT
ou
UPDATE
).
Si aucun rôle n'est indiqué ou si le nom de pseudo rôle
PUBLIC
est utilisé, alors la politique s'applique
à tous les utilisateurs du système. Pour autoriser tous les
utilisateurs à accéder à leurs propres lignes dans une table
utilisateurs
, une simple politique peut être
utilisée :
CREATE POLICY police_utilisateur ON utilisateurs USING (user_name = current_user);
Cela fonctionne de la même manière que dans l'exemple précédent.
Pour utiliser une politique différente pour les lignes ajoutées à la table
de celle appliquée pour les lignes visibles, plusieurs politiques peuvent
être combinées. Cette paire de politiques autorisera tous les utilisateurs
à voir toutes les lignes de la table utilisateurs
, mais
seulement à modifier les leurs :
CREATE POLICY user_sel_policy ON users FOR SELECT USING (true); CREATE POLICY user_mod_policy ON users USING (user_name = current_user);
Pour une commande SELECT
, ces deux politique sont combinées
à l'aide de OR
, ayant pour effet que toutes les lignes
peuvent être sélectionnées. Pour les autres types de commandes, seule la
deuxième politique s'applique, de sorte que les effets sont les mêmes
qu'auparavant.
La sécurité niveau ligne peut également être désactivée avec
la commande ALTER TABLE
. La désactivation de la
sécurité niveau ligne ne supprime pas les polices qui sont définies
pour la table ; elles sont simplement ignorées. L'ensemble des lignes
sont alors visibles et modifiables, selon le système standard des
droits SQL.
Ci-dessous se trouve un exemple plus important de la manière dont
cette fonctionnalité peut être utilisée en production. La table
passwd
simule le fichier des mots de passe d'un
système Unix.
-- Simple exemple basé sur le fichier passwd CREATE TABLE passwd ( user_name text UNIQUE NOT NULL, pwhash text, uid int PRIMARY KEY, gid int NOT NULL, real_name text NOT NULL, home_phone text, extra_info text, home_dir text NOT NULL, shell text NOT NULL ); CREATE ROLE admin; -- Administrateur CREATE ROLE bob; -- Utilisateur normal CREATE ROLE alice; -- Utilisateur normal -- Chargement de la table INSERT INTO passwd VALUES ('admin','xxx',0,0,'Admin','111-222-3333',null,'/root','/bin/dash'); INSERT INTO passwd VALUES ('bob','xxx',1,1,'Bob','123-456-7890',null,'/home/bob','/bin/zsh'); INSERT INTO passwd VALUES ('alice','xxx',2,1,'Alice','098-765-4321',null,'/home/alice','/bin/zsh'); -- Assurez-vous d'activer le row level security pour la table ALTER TABLE passwd ENABLE ROW LEVEL SECURITY; -- Créer les polices -- L'administrateur peut voir toutes les lignes et en ajouter comme il le souhaite CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true); -- Les utilisateurs normaux peuvent voir toutes les lignes CREATE POLICY all_view ON passwd FOR SELECT USING (true); -- Les utilisateurs normaux peuvent mettre à jour leurs propres lignes, -- tout en limitant les shells qu'ils peuvent choisir CREATE POLICY user_mod ON passwd FOR UPDATE USING (current_user = user_name) WITH CHECK ( current_user = user_name AND shell IN ('/bin/bash','/bin/sh','/bin/dash','/bin/zsh','/bin/tcsh') ); -- Donner à admin tous les droits normaux GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin; -- Les utilisateurs ne peuvent que sélectionner des colonnes publiques GRANT SELECT (user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell) ON passwd TO public; -- Autoriser les utilisateurs à mettre à jour certaines colonnes GRANT UPDATE (pwhash, real_name, home_phone, extra_info, shell) ON passwd TO public;
Comme avec tous les réglages de sécurité, il est important de tester et de s'assurer que le système se comporte comme attendu. En utilisant l'exemple ci-dessus, les manipulations ci-dessous montrent que le système des droits fonctionne correctement.
-- admin peut voir toutes les lignes et les colonnes postgres=> set role admin; SET postgres=> table passwd; user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell -----------+--------+-----+-----+-----------+--------------+------------+-------------+----------- admin | xxx | 0 | 0 | Admin | 111-222-3333 | | /root | /bin/dash bob | xxx | 1 | 1 | Bob | 123-456-7890 | | /home/bob | /bin/zsh alice | xxx | 2 | 1 | Alice | 098-765-4321 | | /home/alice | /bin/zsh (3 rows) -- Tester ce que Alice est capable de faire: postgres=> set role alice; SET postgres=> table passwd; ERROR: permission denied for table passwd postgres=> select user_name,real_name,home_phone,extra_info,home_dir,shell from passwd; user_name | real_name | home_phone | extra_info | home_dir | shell -----------+-----------+--------------+------------+-------------+----------- admin | Admin | 111-222-3333 | | /root | /bin/dash bob | Bob | 123-456-7890 | | /home/bob | /bin/zsh alice | Alice | 098-765-4321 | | /home/alice | /bin/zsh (3 rows) postgres=> update passwd set user_name = 'joe'; ERROR: permission denied for table passwd -- Alice est autorisée à modifier son propre nom (real_name), mais pas celui des autres postgres=> update passwd set real_name = 'Alice Doe'; UPDATE 1 postgres=> update passwd set real_name = 'John Doe' where user_name = 'admin'; UPDATE 0 postgres=> update passwd set shell = '/bin/xx'; ERROR: new row violates WITH CHECK OPTION for "passwd" postgres=> delete from passwd; ERROR: permission denied for table passwd postgres=> insert into passwd (user_name) values ('xxx'); ERROR: permission denied for table passwd -- Alice peut modifier son propre mot de passe; RLS empêche silencieusement la mise à jour d'autres lignes postgres=> update passwd set pwhash = 'abc'; UPDATE 1
Toutes les politiques construites jusqu'à maintenant étaient des politiques
permissives, ce qui veut dire que quand plusieurs politiques sont appliquées,
elles sont combinées en utilisant l'opérateur booléen « OR ».
Bien que les politiques permissives puissent être construites pour autoriser
l'accès à des lignes dans les cas attendus, il peut être plus simple de
combiner des politiques permissives avec des politiques restrictives (que
l'enregistrement doit passer et qui sont combinées en utilisant l'opérateur
booléen « AND »). En continuant sur l'exemple ci-dessus, nous
ajoutons une politique restrictive pour exiger que l'administrateur soit
connecté via un socket unix locale pour accéder aux enregistrements de la
table passwd
:
CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin USING (pg_catalog.inet_client_addr() IS NULL);
Nous pouvons alors voir qu'un administrateur se connectant depuis le réseau ne verra aucun enregistrement, du fait de la politique restrictive :
=> SELECT current_user; current_user -------------- admin (1 row) => select inet_client_addr(); inet_client_addr ------------------ 127.0.0.1 (1 row) => SELECT current_user; current_user -------------- admin (1 row) => TABLE passwd; user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell -----------+--------+-----+-----+-----------+------------+------------+----------+------- (0 rows) => UPDATE passwd set pwhash = NULL; UPDATE 0
Les vérifications d'intégrité référentielle, telles que les
contraintes d'unicité ou de clefs primaires et les références
de clefs étrangères, passent toujours outre la sécurité niveau
ligne pour s'assurer que l'intégrité des données est maintenue. Une
attention particulière doit être prise lors de la mise en place des
schémas et des politiques de sécurité de niveau ligne pour éviter qu'un
canal caché (covert channel
) ne dévoile des informations
à travers de telles vérifications d'intégrité référentielle.
Dans certains contextes, il est important de s'assurer que la
sécurité niveau ligne n'est pas appliquée. Par exemple, lors d'une
sauvegarde, il serait désastreux si la sécurité niveau ligne
avait pour conséquence de soustraire silencieusement certaines lignes
de la sauvegarde. Dans une telle situation, vous pouvez positionner
le paramètre de configuration row_security
à off
. En lui-même, ce paramètre ne contourne pas la
sécurité niveau ligne ; ce qu'il fait, c'est qu'il lève une erreur si
le résultat d'une des requêtes venait à être filtrée par une politique. La
raison de l'erreur peut alors être recherchée et résolue.
Dans les exemples ci-dessus, les expressions attachées aux politiques
considèrent uniquement les valeurs de la ligne courante accédée
ou modifiée. C'est le cas le plus simple et le plus performant ;
lorsque c'est possible, il est préférable de concevoir les
applications qui utilisent cette fonctionnalité de la sorte. S'il
est nécessaire de consulter d'autres lignes ou tables pour que
la politique puisse prendre une décision, ceci peut être réalisé
en utilisant dans les expressions des politiques des sous-requêtes
SELECT
ou des fonctions qui contiennent des
commandes SELECT
.
Cependant, faites attention que de tels accès ne créent pas des accès
concurrents qui pourraient permettre une fuite d'informations si aucune
précaution n'est prise. À titre d'exemple, considérez la création de la table
suivante :
-- définition des droits de groupes CREATE TABLE groupes (groupe_id int PRIMARY KEY, nom_groupe text NOT NULL); INSERT INTO groupes VALUES (1, 'bas'), (2, 'moyen'), (5, 'haut'); GRANT ALL ON groupes TO alice; -- alice est l'administratrice GRANT SELECT ON groupes TO public; -- définition des niveaux de droits utilisateurs CREATE TABLE utilisateurs (nom_utilisateur text PRIMARY KEY, groupe_id int NOT NULL REFERENCES groupes); INSERT INTO utilisateurs VALUES ('alice', 5), ('bob', 2), ('mallory', 2); GRANT ALL ON utilisateurs TO alice; GRANT SELECT ON utilisateurs TO public; -- table contenant l'information à protéger CREATE TABLE information (info text, groupe_id int NOT NULL REFERENCES groupes); INSERT INTO information VALUES ('peu secret', 1), ('légèrement secret', 2), ('très secret', 5); ALTER TABLE information ENABLE ROW LEVEL SECURITY; -- une ligne devrait être visible et modifiable pour les utilisateurs -- dont le groupe_id est égal ou plus grand au groupe_id de la ligne CREATE POLICY fp_s ON information FOR SELECT USING (groupe_id <= (SELECT groupe_id FROM utilisateurs WHERE nom_utilisateur = current_user)); CREATE POLICY fp_u ON information FOR UPDATE USING (groupe_id <= (SELECT groupe_id FROM utilisateurs WHERE nom_utilisateur = current_user)); -- nous comptons sur les RLS pour protéger la table information GRANT ALL ON information TO public;
Maintenant, supposez qu'alice
souhaite modifier
l'information « légèrement secrète », mais décide que
mallory
ne devrait pas pouvoir obtenir ce nouveau
contenu, elle le fait ainsi :
BEGIN; UPDATE utilisateurs SET groupe_id = 1 WHERE nom_utilisateur = 'mallory'; UPDATE information SET info = 'caché à mallory' WHERE groupe_id = 2; COMMIT;
Ceci semble correct, il n'y a pas de fenêtre pendant laquelle
mallory
devrait pouvoir accéder à la chaîne
« caché à mallory ». Cependant, il y a une situation de
compétition ici. Si mallory
fait en parallèle,
disons :
SELECT * FROM information WHERE groupe_id = 2 FOR UPDATE;
et sa transaction est en mode READ COMMITED
, il est
possible qu'elle voie « caché à mallory ». C'est possible
si sa transaction accède la ligne information
juste après qu'alice
l'ait fait. Elle est
bloquée en attendant que la transaction d'alice
valide, puis récupère la ligne mise à jour grâce à la clause
FOR UPDATE
. Cependant, elle ne récupère
pas une ligne mise à jour pour la commande
implicite SELECT
sur la table utilisateurs parce
que cette sous-commande n'a pas la clause FOR UPDATE
;
à la place, la ligne utilisateurs
est lue avec
une image de la base de données prise au début de la requête. Ainsi,
l'expression de la politique teste l'ancienne valeur du niveau de droit
de mallory
et l'autorise à voir la valeur mise
à jour.
Il y a plusieurs solutions à ce problème. Une simple réponse
est d'utiliser SELECT ... FOR SHARE
dans
la sous-commande SELECT
de la politique de
sécurité niveau ligne. Cependant, ceci demande de donner le
droit UPDATE
sur la table référencée (ici
utilisateurs
) aux utilisateurs concernés, ce
qui peut ne pas être souhaité. (Une autre politique de sécurité
niveau ligne pourrait être mise en place pour les empêcher d'exercer ce
droit ; ou la sous-commande SELECT
pourrait
être incluse dans une fonction marquée security definer
).
De plus, l'utilisation intensive concurrente de verrous partagés sur les
lignes de la table référencée pourrait poser un problème de
performance, tout spécialement si des mises à jour de cette table sont
fréquentes. Une autre solution envisageable, si les mises à jour de
la table référencée ne sont pas fréquentes, est de prendre un
verrou de type ACCESS EXCLUSIVE
sur la table
référencée lors des mises à jour,
de telle manière qu'aucune autre transaction concurrente ne pourrait
consulter d'anciennes valeurs. Ou une transaction pourrait attendre que
toutes les transactions se terminent après avoir validé une mise
à jour de la table référencée et avant de faire des modifications
qui reposent sur la nouvelle politique de sécurité.
Pour plus de détails, voir CREATE POLICY et ALTER TABLE.