Comment implémenter les autorisations de logique métier dans PostgreSQL (ou SQL en général)?

16

Supposons que j'ai un tableau d'articles:

CREATE TABLE items
(
    item serial PRIMARY KEY,
    ...
);

Maintenant, je veux introduire le concept d '"autorisations" pour chaque élément (veuillez noter que je ne parle pas ici des autorisations d'accès à la base de données, mais des autorisations de logique métier pour cet élément). Chaque élément a des autorisations par défaut et également des autorisations par utilisateur qui peuvent remplacer les autorisations par défaut.

J'ai essayé de réfléchir à plusieurs façons de mettre en œuvre cela et j'ai trouvé les solutions suivantes, mais je ne sais pas laquelle est la meilleure et pourquoi:

1) La solution booléenne

Utilisez une colonne booléenne pour chaque autorisation:

CREATE TABLE items
(
    item serial PRIMARY KEY,

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),

    PRIMARY KEY(item, user),

    can_change_description boolean NOT NULL,
    can_change_price boolean NOT NULL,
    can_delete_item_from_store boolean NOT NULL,
    ...
);

Les avantages : chaque autorisation est nommée.

Désavantages : il existe des dizaines d'autorisations, ce qui augmente considérablement le nombre de colonnes et vous devez les définir deux fois (une fois dans chaque tableau).

2) La solution entière

Utilisez un entier et traitez-le comme un champ de bits (c'est-à-dire que le bit 0 est pour can_change_description, le bit 1 est pour can_change_price, etc., et utilisez des opérations au niveau du bit pour définir ou lire les autorisations).

CREATE DOMAIN permissions AS integer;

Avantages : très rapide.

Inconvénients : vous devez garder une trace de quel bit représente quelle autorisation à la fois dans la base de données et dans l'interface frontale.

3) La solution Bitfield

Identique à 2), mais utilisez bit(n). Probablement les mêmes avantages et inconvénients, peut-être légèrement plus lents.

4) La solution Enum

Utilisez un type d'énumération pour les autorisations:

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

puis créez une table supplémentaire pour les autorisations par défaut:

CREATE TABLE item_default_permissions
(
    item int NOT NULL REFERENCES items(item),
    perm permission NOT NULL,

    PRIMARY KEY(item, perm)
);

et changez la table de définition par utilisateur en:

CREATE TABLE item_per_user_permissions
(
    item int NOT NULL REFERENCES items(item),
    user int NOT NULL REFERENCES users(user),
    perm permission NOT NULL,

    PRIMARY KEY(item, user, perm)    
);

Avantages : Facile à nommer les autorisations individuelles (vous n'avez pas à gérer les positions des bits).

Désavantages : même lors de la récupération des autorisations par défaut, il nécessite d'accéder à deux tables supplémentaires: premièrement, la table des autorisations par défaut et deuxièmement, le catalogue système stockant les valeurs d'énumération.

Surtout parce que les autorisations par défaut doivent être récupérées pour chaque vue de page unique de cet élément , l'impact sur les performances de la dernière alternative peut être significatif.

5) La solution d'Enum Array

Identique à 4), mais utilisez un tableau pour conserver toutes les autorisations (par défaut):

CREATE TYPE permission AS ENUM ('can_change_description', 'can_change_price', .....);

CREATE TABLE items
(
    item serial PRIMARY KEY,

    granted_permissions permission ARRAY,
    ...
);

Avantages : Facile à nommer les autorisations individuelles (vous n'avez pas à gérer les positions des bits).

Inconvénients : casse la 1ère forme normale et est un peu moche. Prend un nombre considérable d'octets d'affilée si le nombre d'autorisations est important (environ 50).

Pouvez-vous penser à d'autres alternatives?

Quelle approche adopter et pourquoi?

Remarque: il s'agit d'une version modifiée d'une question publiée précédemment sur Stackoverflow .

JohnCand
la source
2
Avec des dizaines d'autorisations différentes, je pourrais choisir un (ou plusieurs) bigintchamps (chacun valable pour 64 bits) ou une chaîne de bits. J'ai écrit quelques réponses connexes sur SO qui pourraient être utiles.
Erwin Brandstetter

Réponses:

7

Je sais que vous ne posez pas de questions sur la sécurité des bases de données en soi , mais vous pouvez faire ce que vous voulez en utilisant la sécurité des bases de données. Vous pouvez même l'utiliser dans une application Web. Si vous ne souhaitez pas utiliser la sécurité de la base de données, les schémas s'appliquent toujours.

Vous souhaitez une sécurité au niveau des colonnes, une sécurité au niveau des lignes et probablement une gestion hiérarchique des rôles. La sécurité basée sur les rôles est beaucoup plus facile à gérer que la sécurité basée sur les utilisateurs.

Cet exemple de code est pour PostgreSQL 9.4, qui sort bientôt. Vous pouvez le faire avec 9.3, mais il faut plus de travail manuel.

Vous voulez que tout soit indexable si vous êtes préoccupé par la performance †, ce que vous devriez être. Cela signifie que les champs de masque de bits et de tableau ne seront probablement pas une bonne idée.

Dans cet exemple, nous conservons les tables de données principales dans le dataschéma et les vues correspondantes dans public.

create schema data; --main data tables
create schema security; --acls, security triggers, default privileges

create table data.thing (
  thing_id int primary key,
  subject text not null, --or whatever
  owner name not null
);

Mettez un déclencheur sur data.thing pour les insertions et les mises à jour en faisant en sorte que la colonne propriétaire soit l'utilisateur_current. Peut-être n'autorisez que le propriétaire à supprimer ses propres enregistrements (un autre déclencheur).

Créez une WITH CHECK OPTIONvue, ce que les utilisateurs utiliseront réellement. Essayez vraiment de le mettre à jour, sinon vous aurez besoin de déclencheurs / règles, ce qui est plus de travail.

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner,
from data.thing
where
pg_has_role(owner, 'member') --only owner or roles "above" him can view his rows. 
WITH CHECK OPTION;

Ensuite, créez une table de liste de contrôle d'accès:

--privileges r=read, w=write

create table security.thing_acl (
  thing_id int,
  grantee name, --the role to whom your are granting the privilege
  privilege char(1) check (privilege in ('r','w') ),

  primary key (thing_id, grantee, privilege),

  foreign key (thing_id) references data.thing(thing_id) on delete cascade
);

Modifiez votre vue pour tenir compte des ACL:

drop view public.thing;

create view public.thing with(security_barrier) as 
select
thing_id,
subject,
owner
from data.thing a
where
pg_has_role(owner, 'member')
or exists (select 1 from security.thing_acl b where b.thing_id = a.thing_id and pg_has_role(grantee, 'member') and privilege='r')
with check option;

Créez une table de privilèges de ligne par défaut:

create table security.default_row_privileges (
  table_name name,
  role_name name,
  privilege char(1),

  primary key (table_name, role_name, privilege)
);

Mettez un déclencheur sur insert sur data.thing pour qu'il copie les privilèges de ligne par défaut dans security.thing_acl.

  • Ajustez la sécurité au niveau de la table de manière appropriée (empêchez les insertions d'utilisateurs indésirables). Personne ne devrait pouvoir lire les données ou les schémas de sécurité.
  • Ajustez la sécurité au niveau des colonnes de manière appropriée (empêchez certains utilisateurs de voir / modifier certaines colonnes). Vous pouvez utiliser has_column_privilege () pour vérifier qu'un utilisateur peut voir une colonne.
  • Vous voulez probablement une balise de définition de sécurité sur votre vue.
  • Pensez à ajouter grantoret admin_optioncolonnes à des tables acl pour suivre qui a accordé le privilège, et si le bénéficiaire peut gérer des privilèges sur cette ligne.
  • Lots de test

† Dans ce cas, pg_has_role n'est probablement pas indexable. Vous devez obtenir une liste de tous les rôles supérieurs à current_user et les comparer à la valeur propriétaire / bénéficiaire à la place.

Neil McGuigan
la source
Avez-vous vu la partie « Je ne parle pas des autorisations d'accès à la base de données ici »?
a_horse_with_no_name
@a_horse_with_no_name oui je l'ai fait. Il peut écrire son propre système RLS / ACL ou utiliser la sécurité intégrée d'une base de données pour faire ce qu'il demande.
Neil McGuigan
Merci pour votre réponse détaillée! Cependant, je ne pense pas que l'utilisation des rôles de base de données soit la bonne réponse ici, car non seulement le personnel, mais également chaque utilisateur peut avoir des autorisations. Des exemples seraient «can_view_item», «can_bulk_order_item» ou «can_review_item». Je pense que mon choix initial de noms d'autorisation vous a amené à croire qu'il ne s'agissait que d'autorisations du personnel, mais tous ces noms n'étaient que des exemples pour résumer les complexités. Comme je l'ai dit dans la question d'origine, il s'agit d' autorisations par utilisateur et non par autorisations du personnel .
JohnCand
Quoi qu'il en soit, devoir avoir des rôles de base de données distincts pour chaque ligne utilisateur dans la table des utilisateurs semble être excessif et difficilement gérable. Cependant, je pense que votre réponse est précieuse pour les développeurs qui implémentent uniquement les autorisations du personnel.
JohnCand du
1
@JohnCand Je ne vois pas vraiment comment il est plus facile de gérer les autorisations ailleurs, mais veuillez nous indiquer votre solution une fois que vous l'avez trouvée! :)
Neil McGuigan
4

Avez-vous envisagé d'utiliser la liste de contrôle d'accès extension PostgreSQL de la ?

Il contient le type de données natif PostgreSQL ACE et un ensemble de fonctions qui vous permettent de vérifier si un utilisateur est autorisé à accéder aux données. Il fonctionne soit avec le système de rôles PostgreSQL, soit avec des numéros abstraits (ou UUID) représentant les ID utilisateur / rôle de votre application.

Dans votre cas, vous ajoutez simplement une colonne ACL à vos tables de données et utilisez l'une des acl_check_accessfonctions pour comparer un utilisateur à une ACL.

CREATE TABLE items
(
    item serial PRIMARY KEY,
    acl ace[],
    ...
);

INSERT INTO items(acl, ...) VALUES ('{a//<user id>=r, a//<role id>=rwd, ...}');

SELECT * FROM items where acl_check_access(acl, 'r', <roles of the user>, false) = 'r'

L'utilisation des listes de contrôle d'accès est un moyen extrêmement flexible de gérer les autorisations de logique métier. De plus, il est incroyablement rapide - le temps système moyen n'est que de 25% du temps nécessaire pour lire un enregistrement. La seule limitation étant qu'il prend en charge un maximum de 16 autorisations personnalisées par type d'objet.

Slonopotamus
la source
1

Je peux penser à une autre possibilité de coder cela, la relationnelle

Si vous n'avez pas besoin permission_per_itemtableyou peut sauter et se connecter Permissionset Itemsdirectement à la item_per_user_permissionstable.

entrez la description de l'image ici

diagramme de légende

miracle173
la source