web-dev-qa-db-fra.com

Schéma de base de données pour ACL

Je souhaite créer un schéma pour une liste de contrôle d'accès. Cependant, je suis partagé entre deux façons de le mettre en œuvre.

Je suis à peu près sûr de ne pas vouloir gérer les autorisations en cascade, car cela entraîne beaucoup de confusion pour le back-end et les administrateurs de site.

Je pense aussi que je ne peux vivre avec des utilisateurs que dans un rôle à la fois. Une configuration comme celle-ci permettra l'ajout de rôles et d'autorisations selon les besoins, à mesure que le site se développe, sans affecter les rôles/règles existants.

Au début, j'allais normaliser les données et disposer de trois tables pour représenter les relations.

ROLES { id, name }
RESOURCES { id, name }
PERMISSIONS { id, role_id, resource_id }

Une requête permettant de déterminer si un utilisateur était autorisé quelque part ressemblerait à ceci:

SELECT id FROM resources WHERE name = ?
SELECT * FROM permissions WHERE role_id = ? AND resource_id = ? ($user_role_id, $resource->id)

Ensuite, j'ai réalisé que je ne disposerais que d'environ 20 ressources, chacune comportant jusqu'à 5 actions (créer, mettre à jour, afficher, etc.) et éventuellement 8 autres rôles. Cela signifie que je peux faire preuve d'un mépris flagrant pour la normalisation des données car je n'aurai jamais plus de deux cents enregistrements possibles.

Alors, peut-être qu'un tel schéma aurait plus de sens.

ROLES { id, name }
PERMISSIONS { id, role_id, resource_name }

ce qui me permettrait de rechercher des enregistrements dans une seule requête

SELECT * FROM permissions WHERE role_id = ? AND permission  = ? ($user_role_id, 'post.update')

Alors lequel est le plus correct? Existe-t-il d'autres structures de schéma pour ACL?

26
Xeoncross

D'après mon expérience, la vraie question consiste essentiellement à déterminer si une quelconque restriction d'accès spécifique à l'utilisateur sera appliquée.

Supposons, par exemple, que vous conceviez le schéma d'une communauté et que vous autorisiez les utilisateurs à modifier la visibilité de leur profil.

Une option consiste à s'en tenir à un indicateur de profil public/privé et à un contrôle d'autorisation exhaustif et préventif: "utilisateurs.view" (visualise les utilisateurs publics) par exemple, "utilisateurs.view_all" (visualise tous les utilisateurs, pour les modérateurs). .

Une autre implique des autorisations plus fines. Vous voudrez peut-être qu’ils soient capables de configurer les choses de manière à ce qu’ils puissent se rendre visibles par tous, b) visibles par leurs copains triés sur le volet, c) totalement confidentiels et peut-être ) visibles par tous à l'exception de leurs bozos cueillis à la main. Dans ce cas, vous devez stocker des données relatives au propriétaire/à l'accès pour des lignes individuelles, et vous devez en extraire énormément pour éviter de matérialiser la fermeture transitive d'un graphe orienté dense.

Quelle que soit l'approche choisie, j'ai constaté que la complexité supplémentaire liée à la modification/affectation des rôles est compensée par la facilité/flexibilité résultante des autorisations attribution sur des données individuelles.

  1. Les utilisateurs peuvent avoir plusieurs rôles
  2. Rôles et autorisations fusionnés dans le même tableau avec un drapeau pour distinguer les deux (utile lors de l'édition de rôles/permanentes)
  3. Les rôles peuvent affecter d'autres rôles, et les rôles et les perms peuvent attribuer des autorisations (mais les autorisations ne peuvent pas attribuer de rôles), à partir de la même table.

Le graphe orienté résultant peut ensuite être extrait en deux requêtes, construit une fois pour toutes en un temps raisonnable en utilisant la langue que vous utilisez, et mis en cache dans Memcache ou similaire pour une utilisation ultérieure.

À partir de là, extraire les autorisations d'un utilisateur consiste à vérifier ses rôles et à les traiter à l'aide du graphique d'autorisation afin d'obtenir les autorisations finales. Vérifiez les autorisations en vérifiant qu'un utilisateur a ou non le rôle/l'autorisation spécifié. Et puis exécutez votre requête/émettez une erreur en fonction de cette vérification des autorisations.

Vous pouvez étendre la vérification pour des nœuds individuels (c'est-à-dire que check_perms($user, 'users.edit', $node) pour "peut éditer ce nœud" par rapport à check_perms($user, 'users.edit') pour "peut éditer un nœud") si vous en avez besoin, et vous aurez quelque chose de très flexible/facile à utiliser pour les utilisateurs finaux.

Comme le montre l'exemple d'ouverture, évitez de trop vous orienter vers des autorisations au niveau de la ligne. Le goulot d'étranglement des performances réside moins dans la vérification des autorisations d'un nœud individuel que dans l'extraction d'une liste de nœuds valides (c'est-à-dire uniquement ceux que l'utilisateur peut afficher ou modifier). Je vous déconseille tout ce qui va au-delà des indicateurs et des champs user_id dans les lignes elles-mêmes si vous n'êtes pas (très) versé dans l'optimisation des requêtes.

28

Cela signifie que je peux exercer un mépris flagrant sur la normalisation des données, étant donné que je n'aurai jamais plus de deux […] cent enregistrements possibles.

Le nombre de lignes que vous attendez n'est pas un critère pour choisir la forme normale à viser. La normalisation concerne l’intégrité des données. Cela augmente généralement l’intégrité des données en réduisant la redondance. 

La vraie question à poser n'est pas "Combien de lignes vais-je avoir?", Mais "Dans quelle mesure est-il important que la base de données me donne toujours les bonnes réponses?" Pour une base de données qui sera utilisée pour implémenter une liste de contrôle d'accès, je dirais "Pretty danged important."

Au contraire, un faible nombre de lignes suggère que vous n'avez pas besoin de vous soucier de la performance. Le choix de 5NF devrait donc être un choix facile. Vous voudrez frapper 5NF avant d'ajouter des identifiants.

Une requête permettant de déterminer si un utilisateur était autorisé. Aurait dû ressembler à :

SELECT id FROM resources WHERE name = ?
SELECT * FROM permissions 
WHERE role_id = ? AND resource_id = ? ($user_role_id, $resource->id)

Le fait que vous ayez écrit cela sous forme de deux requêtes au lieu d'utiliser une jointure interne suggère que vous êtes peut-être au-dessus de votre tête. (C'est une observation, pas une critique.)

SELECT p.* 
FROM permissions p
INNER JOIN resources r ON (r.id = p.resource_id AND 
                           r.name = ?)

Vous pouvez utiliser un SET pour assigner les rôles.

CREATE TABLE permission (
  id integer primary key autoincrement
  ,name varchar
  ,perm SET('create', 'edit', 'delete', 'view')
  ,resource_id integer );
0
Johan