Gestion des droits d’accès par groupes d’utilisateurs avec ACL
Pour faire suite à notre tutorial sur l’authentification simple avec le Composant Auth, nous allons mettre en place une gestion de droits d’accès différents selon des groupes d’utilisateurs, en utilisant les ACL (Access Control Lists). Nous partons du principe que le lecteur a déjà mis en place l’authentification décrite dans le tutorial.
Nos besoins sont les suivants :
- les Articles sont accessibles en consultation par tous ;
- l’administration est accessible à l’adresse /admin et requiert une authentification ;
- l’administration sert à gérer les Groupes, les Utilisateurs et les Articles ;
- nous distinguons deux groupes d’utilisateurs : Administration et Rédaction ;
- un Utilisateur appartient à un seul Groupe ;
- les administrateurs ont droit à tout ;
- les rédacteurs n’ont droit qu’à la gestion des Articles.
Note importante : pour les besoins de ce tutorial, la page d’accueil de l’administration est maintenant gérée par l’action admin_menu du Contrôleur UsersController, et non plus par l’action admin_index du Contrôleur ArticlesController. Ce changement, quoique mineur, devait être mentionné pour une bonne compréhension de ce sujet.
1. Principe
CakePHP intègre un outil de gestion de Permissions d’accès à des Ressources par des Rôles.
Une Ressource peut représenter notre Application dans son ensemble, un Contrôleur complet, une Action, ou un enregistrement d’un Modèle.
Un Rôle peut représenter un Groupe d’Utilisateurs ou un Utilisateur précis.
Une Permission représente les opérations que peut effectuer un Rôle sur une Ressource. Ces opérations sont les quatre opérations de base : create, read, update et delete (CRUD).
2. Mise en place
CakePHP stocke l’Acl dans trois tables :
aros(Access Role Object : les Rôles) ;acos(Access Control Object : les Ressources) ;aros_acos(relations entre AROs et ACOs : les Permissions).
Nous allons d’autre part enregistrer les Groupes d’Utilisateurs dans la table groups, et modifier notre table existante users pour la relier à la table groups.
2.1. Création des tables
2.1.1. Les tables aros, acos et aros_acos
La création des trois tables aros, acos et aros_acos peut se faire en ligne de commande avec la console de CakePHP :
cake schema run create DbAcl
ou simplement en important le script {app}/config/sql/db_acl.sql dans la base de données.
2.1.2. La table groups
CREATE TABLE `groups` ( `id` mediumint(8) UNSIGNED NOT NULL AUTO_INCREMENT, `parent_id` mediumint(8) UNSIGNED DEFAULT NULL, `name` varchar(255) NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`), KEY `parent_id` (`parent_id`) ) ENGINE=MyISAM;
Remarque : le champ parent_id sert à créer une arborescence de Groupes qui peut être utilisée automatiquement par l’Acl : en effet, CakePHP est capable de remonter cette arborescence pour trouver la Permission d’un Groupe donné.
Nous insérons ensuite nos deux groupes : Administration et Rédaction. Nous n’avons pas besoin dans notre étude d’une arborescence de Groupes, le parent_id est donc nul.
INSERT INTO `groups` (`id`, `parent_id`, `name`, `created`) VALUES (1, NULL, 'Administration', NOW()), (2, NULL, 'Rédaction', NOW());
2.1.3. Modification de la table users
ALTER TABLE `users` ADD `group_id` MEDIUMINT UNSIGNED NOT NULL AFTER `id`; ALTER TABLE `users` ADD INDEX (`group_id`);
Nous modifions l’Utilisateur déjà présent (admin) pour l’affecter au Groupe Administration :
UPDATE `users` SET `group_id` = 1 WHERE `id` = 1;
Nous ajoutons un nouvel Utilisateur dans le Groupe Rédaction, avec pour login redacteur et mot de passe redacteur :
INSERT INTO `users` (`id`, `group_id`, `login`, `password`, `disabled`, `created`) VALUES (2, 2, 'redacteur', SHA1('6a10cdde80fb56150efda09365f91579ea74a944redacteur'), 0, NOW());
2.2. Création des modèles
Nous allons créer le Modèle Group et modifier le Modèle User existant.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // {app}/models/group.php class Group extends AppModel { var $name = 'Group'; var $actsAs = array('Acl'); var $hasMany = 'User'; function parentNode() { if (!$this->id) { return null; } $data = $this->read(); if(!$data['Group']['parent_id']) { return null; } return array( 'model' => 'Group', 'foreign_key' => $data['Group']['parent_id'] ); } } |
Quelques explications :
var $actsAs = array('Acl');
Le Comportement Acl sert à lire, créer, modifier et supprimer les Permissions de l’enregistrement du Modèle.- la méthode
parentNode()renvoie l’id du Groupe parent : CakePHP s’en sert pour parcourir l’arborescence jusqu’à trouver les Permissions qui s’y rapportent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // {app}/models/user.php class User extends AppModel { var $name = 'User'; var $actsAs = array('Acl'); var $belongsTo = 'Group'; function parentNode() { return null; } function bindNode($object) { return array( 'model' => 'Group', 'foreign_key' => $object['User']['group_id'] ); } } |
Quelques explications :
- La méthode
bindNode($object)sert à associer l’Utilisateur en cours à son Groupe. Le paramètre$objectcontient les données de l’Utilisateur en cours, et la fonction renvoie son Groupe. Cela évite de devoir créer un Rôle pour chaque Utilisateur : CakePHP est ainsi capable d’appliquer des Permissions à un Utilisateur en fonction du Rôle associé à son Groupe. - La méthode
parentNode()renvoie null car il n’y a pas d’arborescence d’Utilisateurs. Cette méthode est requise, car contrairement àbindNode(), CakePHP ne vérifie pas la définition de la méthode dans le Modèle avant de l’invoquer. On peut imaginer placer la méthodeparentNode()dans le fichierapp_model.phpet la redéfinir uniquement dans les Modèles arborescents (comme le Modèle Groupe).
2.3 Import de l’ACL
Nous allons créer un Contrôleur temporaire qui ne sera appelé qu’une seule fois, et dont le but est d’insérer dans la base de données les Rôles, les Ressources et les Permissions.
Nous appellerons l’adresse /init_acl/init pour lancer le script. Nous pourrons ensuite le supprimer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | class InitAclController extends AppController { var $name = 'InitAcl'; var $components = array('Acl'); var $uses = array('Aro', 'Aco'); function createAro($model, $foreign_key, $parent_id, $alias) { $this->Aro->create(); $this->Aro->save( array( 'model'=>$model, 'foreign_key'=>$foreign_key, 'parent_id'=>$parent_id, 'alias'=>$alias ) ); return $this->Aro->id; } function createAco($model, $foreign_key, $parent_id, $alias) { $this->Aco->create(); $this->Aco->save( array( 'model'=>$model, 'foreign_key'=>$foreign_key, 'parent_id'=>$parent_id, 'alias'=>$alias ) ); return $this->Aco->id; } function deleteDB() { $this->Aro->query("TRUNCATE acos"); $this->Aro->query("TRUNCATE aros"); $this->Aro->query("TRUNCATE aros_acos"); } function message($mssg) { echo $mssg.'<br/>'; } function init() { $this->message("Initialisation des droits d'acces..."); $this->message("Suppression des droits existants..."); $this->deleteDB(); // AROs // ---- // |-Administrateur (group_id=1) // |-Redacteur (group_id=2) $this->message("Creation des AROs..."); $this->createAro('Group', 1, null, 'Administrateur'); $this->createAro('Group', 2, null, 'Redacteur'); // ACOs // ---- // |-Application // |-Groups // |-Users // |-admin_menu // |-Articles $this->message("Creation des ACOs..."); $Application_id = $this->createAco(null, null, null, 'Application'); $this->createAco(null, null, $Application_id, 'Groups'); $Users_id = $this->createAco(null, null, $Application_id, 'Users'); $this->createAco(null, null, $Users_id, 'admin_menu'); $this->createAco(null, null, $Application_id, 'Articles'); // AROs-ACOs // --------- // Administrateur - Application, all // Redacteur - Articles, all // Redacteur - admin_menu, all $this->message("Creation des permissions..."); $this->Acl->allow('Administrateur', 'Application', '*'); $this->Acl->allow('Redacteur', 'Articles', '*'); $this->Acl->allow('Redacteur', 'admin_menu', '*'); $this->message("Initialisation terminee."); $this->autoRender = false; } } |
Quelques explications :
var $components = array('Acl');
Le Composant Acl permet d’insérer les Permissions dans la table aros_acos.var $uses = array('Aro', 'Aco');
Les modèles Aro et Aco permettent d’insérer les Rôles dans la table aros et les Ressources dans la table acos.- les fonctions
createAroetcreateAcoattendent quatre paramètres :$model: nom du Modèle associé au Rôle (Group ou User) ou à la Ressource (Article) ;$foreign_key: id de l’enregistrement du Modèle concerné ;$parent_id: id du Rôle ou de la Ressource parent pour gérer l’héritage des Permissions ;$alias: nom (unique) du Rôle ou de la Ressource.
- la méthode
init()crée les Rôles, les Ressources, et enfin les Permissions associées :$this->Acl->allow('Administrateur', 'Application', '*');
Le Rôle Administrateur (associé au Groupe Administration) a accès à toute l’Application (Contrôleurs, actions, enregistrements) avec toutes les opérations CRUD.$this->Acl->allow('Redacteur', 'Articles', '*');
Le Rôle Redacteur a accès à tout le Contrôleur Articles (actions, enregistrements) avec toutes les opération CRUD.$this->Acl->allow('Redacteur', 'admin_menu', '*');
Le Rôle Redacteur a accès au menu de l’administration qui est une action du Contrôleur User, normalement interdit au Rôle Redacteur (à qui nous n’avons autorisé l’accès qu’aux articles). Notons que pour une action, le fait de ne pas donner toutes les Permissions d’opérations CRUD avec ‘*’ en interdit l’accès. Les paramètres représentés par ‘*’ étant finalement eux-mêmes des actions, interdire par exemple l’opération ‘delete’ sur l’actionindexdu ContrôleurArticlesControllern’aurait aucun sens.
Remarque : pour restreindre une Permission à l’une des opérations de base (create, read, update, delete), nous pouvons passer un array avec les opérations autorisées à la place de ‘*’. Par exemple, pour permettre au Rôle Redacteur de gérer les Articles sans pouvoir les supprimer, nous ferions :
$this->Acl->allow('Redacteur', 'Articles', array('create', 'read', 'update'));
Ou encore plus simplement :
$this->Acl->deny('Redacteur', 'Articles', array('delete'));
3. Intégration avec le Composant Auth
Maintenant que les Rôles, Ressources et Permissions sont en place, nous allons indiquer au Composant Auth de ne plus se baser sur la fonction isAuthorized pour autoriser ou non l’accès à une action, mais de se brancher automatiquement sur l’Acl pour savoir si l’Utilisateur connecté est relié à un Rôle qui lui permet d’accéder à l’opération demandée sur la Ressource. Nous modifions donc le Contrôleur général AppController :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // {app}/controllers/app_controller.php class AppController extends Controller { var $helpers = array ('Html', 'Text', 'Form'); var $components = array('Acl','Auth'); function beforeFilter() { if(isset($this->Auth)) { $this->Auth->userModel = 'User'; $this->Auth->userScope = array('User.disabled' => 0); $this->Auth->fields = array('username' => 'login', 'password' => 'password'); $this->Auth->loginAction = '/users/login'; $this->Auth->loginRedirect = '/admin/users/menu'; $this->Auth->logoutRedirect = '/'; $this->Auth->loginError = "Identifiant ou mot de passe incorrects."; $this->Auth->authError = "Vous n'avez pas accès à cette page."; $this->Auth->autoRedirect = true; $this->Auth->authorize = 'actions'; if((empty($this->params['prefix']) || $this->params['prefix'] != 'admin') && $this->action != 'login') { $this->Auth->allow(); } } } function beforeRender() { if(isset($this->params['prefix']) && $this->params['prefix'] == 'admin') { $this->layout = 'admin_default'; } } } |
Quelques explications :
var $components = array('Acl','Auth');
Attention à l’ordre d’inclusion des Composants, d’abord l’Acl puis l’Auth.$this->Auth->authorize = 'actions';
Cette instruction indique au Composant Auth qu’il doit obtenir les droits d’accès auprès du Composant Acl.if((empty($this->params['prefix']) || $this->params['prefix'] != ‘admin’) && $this->action != ‘login’) { $this->Auth->allow(); }
Nous ajoutons cette étape pour nous dispenser d’ajouter un Rôle pour le simple visiteur : si l’action demandée n’est pas une action d’administration et si l’action n’est pas le login, Auth autorise l’accès sans demander à Acl. Cette logique est liée à nos besoins et peut tout à fait être omise pour laisser à l’Acl le soin de gérer tous les accès, à charge au lecteur de prévoir un Rôle pour un utilisateur non enregistré.
4. Conclusion
La difficulté dans la mise en place d’une solution de gestion d’accès par groupes d’utilisateurs tient surtout aux faits que la documentation actuelle est plutôt ténue à ce sujet et à l’absence d’exemple concret d’une implémentation réelle. Nous espérons que cet article permettra au lecteur de se lancer dans l’étude de cette fonctionnalité, car la souplesse apportée par les Acl (gérer les droits au niveau de l’application complète, d’un Contrôleur, d’une action, d’un enregistrement ou même d’un seul champ d’un enregistrement !) permet de l’inclure par défaut dans tout projet, quels que soient les besoins du client.
Pierre-Emmanuel Fringant et Matthieu Sadouni
Superbe article, une fois de plus !
Peut-on gérer l’appartenance d’un membre à plusieurs groupes ?
Merci pour votre travail qui nous apporte de précieux éclaircissements sur la cuisine de ce bon gâteau !
25 février 2008 à 9:26
Auteur : Avairet