Consultez la FAQ sur le ZF avant de poster une question
Vous n'êtes pas identifié.
Bonjour à tous ;-),
Je viens de terminer mon système d'ACL pour mon site avec ZF 2, et je vous partage le code. N'hésitez pas à me signaler toute remarque (je n'ai pas encore eu le temps de le tester à fond).
Les contraintes que j'avais pour mon système :
* Pouvoir charger les ACL depuis un fichier de configuration.
* Pour facilement ajouter des assertions.
* Ne pas créer l'arbre des autorisations pour toutes les routes, mais uniquement pour la route demandée.
* Pouvoir choisir la page de redirection en cas de refus d'accès à une page.
D'autre part, mon système utilise le nouveau composant ServiceManager, qui remplace avantageusement (en termes de performances) Zend\Di, ainsi que les évènements et les modules.
Je ne ferai pas d'explications trop longues, mais si vous avez des questions, n'hésitez pas.
/** * @param ModuleManager $moduleManager */ public function init(ModuleManager $moduleManager) { $events = $moduleManager->events(); $sharedEvents = $events->getSharedManager(); // Attache l'évènement d'ACL $sharedEvents->attach('Zend\Mvc\Controller\ActionController', MvcEvent::EVENT_DISPATCH, function($e) { $serviceManager = $e->getApplication()->getServiceManager(); $acl = $serviceManager->get('Common\Acl\Acl'); $acl->dispatch($e); }, 100); }
Ce code est à placer dans l'un de vos modules (chez moi, dans un module "Common" qui regroupe toutes les fonctionnalités génériques que je réutilise dans tous mes projets).
Ce petit bout de code se contente d'ajouter un listener à l'évènement MvcEvent::EVENT_DISPATCH (qui est lancé dès qu'une requête est dispatchée), avec une priorité suffisamment élevée pour passer avant les autres (on souhaite effectivement autoriser/refuser l'accès le plus tôt possible).
A noter que l'objet Common\Acl\Acl est récupéré via le service manager. Pour plus d'informations : http://blanchon-vincent.developpez.com/ … fabriques/
Ma fabrique est définie dans le fichier module.config.php du module concerné :
return array( // Service manager 'service_manager' => array( 'factories' => array( 'Common\Acl\Acl' => function($serviceManager) { $authenticationService = $serviceManager->get('Zend\Authentication\AuthenticationService'); $acl = new Acl($serviceManager, $authenticationService); return $acl; }
A noter qu'il aurait été possible de ne pas déclarer de fabrique et de laisser le composant d'injecteur de dépendance injecter les dépendances automatiquement, toutefois cela n'est pas conseillé en terme de performances, c'est d'ailleurs pourquoi ce composant a été introduit dans la bêta 4.
Mon objet ACL prend deux paramètres :
- le service manager, qui nous permettra de récupérer la config de TOUS les modules (par exemple, si vous disposez d'un module "user" et "blog", vous pouvez définir tous les ACL liés au module "user" dans le fichier de config du module User, et les ACL liés au module "blog" dans le fichier de config du module Blog, le servicemanager permettant de récupérer le tout sous forme d'un unique tableau).
- un objet AuthenticationService qui nous permet de récupérer l'utilisateur connecté.
Et enfin, le gros du boulot, l'objet Acl :
namespace Common\Acl; use Zend\Acl\Acl as BaseAcl, Zend\Authentication\AuthenticationService, Zend\Mvc\MvcEvent, Zend\Mvc\Router\RouteMatch, Zend\ServiceManager\ServiceManager; class Acl extends BaseAcl { /** * Rôle par défaut */ const DEFAULT_ROLE = 'guest'; /** * @var \Zend\ServiceManager\ServiceManager */ protected $serviceManager; /** * @var \Zend\Authentication\AuthenticationService */ protected $authenticationService; /** * Route vers laquelle on redirige si les accès ne sont pas valides * * @var string */ protected $redirectTo = 'home'; /** * @param \Zend\ServiceManager\ServiceManager $serviceManager * @param \Zend\Authentication\AuthenticationService $authenticationService */ public function __construct(ServiceManager $serviceManager, AuthenticationService $authenticationService) { $this->serviceManager = $serviceManager; $this->authenticationService = $authenticationService; $this->initRoles(); } /** * @param \Zend\Mvc\MvcEvent $e */ public function dispatch(MvcEvent $e) { $this->build($e->getRouteMatch()); // Récupération de l'utilisateur courant $role = $this->authenticationService->getIdentity(); if ($role == null) { $role = self::DEFAULT_ROLE; } // Si l'utilisateur n'est pas autorisé, on le redirige vers la page par défaut, ou une autre page si elle a été spécifiée // dans le fichier de configuration $matchedRouteName = $e->getRouteMatch()->getMatchedRouteName(); if (!$this->isAllowed($role, $matchedRouteName)) { $this->redirect($e, $this->redirectTo); } } /** * @param \Zend\ServiceManager\ServiceManager $serviceManager * @return Acl */ public function setServiceManager(ServiceManager $serviceManager) { $this->serviceManager = $serviceManager; return $this; } /** * Initialise les rôles à partir du fichier de configuration */ private function initRoles() { $configuration = $this->serviceManager->get('Configuration'); if (isset($configuration['acl']['roles'])) { $roles = $configuration['acl']['roles']; foreach($roles as $role => $parents) { $this->addRole($role, $parents); } } } /** * @param \Zend\Mvc\Router\RouteMatch $routeMatch * @return mixed */ private function build(RouteMatch $routeMatch) { // Récupération de la configuration $configuration = $this->serviceManager->get('Configuration'); $matchedRouteName = $routeMatch->getMatchedRouteName(); if (isset($configuration['acl']['resources'])) { $resources = $configuration['acl']['resources']; $routeParts = explode('/', $routeMatch->getMatchedRouteName()); $parentPart = null; foreach($routeParts as $routePart) { $this->addResource($routePart, $parentPart); // Par défaut, on interdit l'accès à toute ressource dont l'ACL n'a pas été défini if (!isset($resources[$routePart])) { $this->deny(self::DEFAULT_ROLE, $routePart); return; } $resources = $resources[$routePart]; if (isset($resources['allow'])) { $allow = $resources['allow']; $assertion = $this->buildAssertion($allow); $this->allow($allow['roles'], $routePart, null, $assertion); } if (isset($resources['deny'])) { $deny = $resources['deny']; $assertion = $this->buildAssertion($deny); $this->deny($deny['roles'], $routePart, null, $assertion); } if (isset($resources['redirect_to'])) { $this->redirectTo = $resources['redirect_to']; } // Y a-t-il des enfants ? if (!isset($resources['child_resources'])) { break; } $resources = $resources['child_resources']; $parentPart = $routePart; } // On définit la route "complète" comme enfant de la dernière ressource afin d'hériter de ses autorisations if (count($routeParts) > 1) { $this->addResource($matchedRouteName, end($routeParts)); } } } /** * @param array $resourcePart * @return null */ private function buildAssertion(array $resourcePart) { if (isset($resourcePart['assertion'])) { return new $resourcePart['assertion'](); } return null; } /** * @param \Zend\Mvc\MvcEvent $e * @param string $route */ private function redirect(MvcEvent $e, $route) { $url = $e->getRouter()->assemble(array(), array('name' => $route)); $response = $e->getResponse(); $response->headers()->addHeaderLine('Location', $url); $response->setStatusCode(302); $response->sendHeaders(); exit; } }
Tout d'abord la fonction initRoles, qui permet de voir comment fonctionne la configuration (on verra ça plus loin) : elle récupère du fichier de configuration les rôles définis dans l'application, et les ajoute à l'ACL (puisque mon objet hérite de Zend\Acl\Acl).
La fonction dispatch récupère l'utilisateur actuellement connecté, et vérifie s'il a accès à la ressource. Si oui, rien ne se passe, si non, il est redirigé vers une page (la page d'accueil par défaut, mais il est possible de changer ce comportement dans la configuration).
Je passe sur la fonction build qui m'a pris un peu de temps à écrire, sachez juste qu'elle permet de construire l'autorisation à partir de la configuration.
Voyons maintenant comment définir la configuration.
Voici un exemple pour mon module principal :
// Gestion des autorisations 'acl' => array( 'roles' => array( 'guest' => null, 'a' => 'guest', 'b' => 'guest' ), 'resources' => array( 'home' => array( 'allow' => array( 'roles' => array('guest', 'a', 'b') ) ) ) ),
La clé 'acl' contient deux sous-clés : rôles pour définir les différents rôles et leur relation de parenté, et resources. Chaque ressource est identifié par le nom de la route correspondante (il aurait d'ailleurs été possible d'écrire les ACL directement dans les définitions des routes, mais je préfère séparer les responsabilités).
Ici, ma route 'home' contient un tableau 'allow', qui contient lui-même un tableau 'rôles' permettant de définir qui a accès à cette route. A noter que l'on aurait pu écrire array('guest') car 'a' et 'b' hérite de 'guest'.
Comme pour les routes, il est possible de définir des resources enfant :
'page1' => array( 'child_resources' => array( 'subpage1' => array( 'allow' => array( 'roles' => array('guest') ), 'deny' => array( 'roles' => array('a', 'b') ) ) )
Encore une fois, il doit y avoir une correspondance parfaite entre le nom des routes et les noms donnés pour l'ACL. Ainsi, il existe une route nommée page1/subpage1. Dans ce cas, les utilisateurs auront accès à la route 'page1/subpage1' tandis que les utilisateurs 'a' et 'b' n'y auront pas accès, et seront redirigés par défaut vers la page d'accueil (note : il ne faut pas confondre les noms de route du couple contrôleur/action. Ainsi la route page1/subpage1 peut très bien redirieger vers le contrôleur "Index", et l'action "toto", si c'est comme ça que vous l'avez spécifié dans votre fichier de configuration).
Si on souhaite les rediriger vers une autre page, il suffit de rajouter la clé 'redirect_to', avec un nom de route :
'page1' => array( 'child_resources' => array( 'subpage1' => array( 'allow' => array( 'roles' => array('guest') ), 'deny' => array( 'roles' => array('a', 'b') ), 'redirect_to' => 'uneRoute' ) )
Ici, les utilisateurs a et b seront redirigés vers la route 'uneRoute'.
Il est également possible de spécifier une assertion, un objet permettant de spécifier une condition à l'accès ou au refus. Par exemple, on peut décider d'autoriser la page à tous les utilisateurs, SAUF à ceux dont l'IP est banni.
Il suffit pour cela d'ajouter la clé 'assertion' :
'home' => array( 'allow' => array( 'roles' => array('guest', 'a', 'b'), 'assertion' => 'Common\Acl\Assertion\IPTest' ) )
Ou la valeur de la clé 'assertion' est le FQCN de la classe.
Si vous avez des questions sur le fonctionnement interne ou l'utilisation, n'hésitez pas !
Hors ligne
Salut, merci pour ce retour. Je pense que ça me servira .
Par contre j'ai un truc qui me chagrine, au niveau de ton authentificationService tu ne garde que le rôle de l'utilisateur ? Puisque tu transmets $role à la méthode isAllowed et je n'ai pas vu de redéfinition de cette méthode.
Hors ligne
Je crois que je n'ai pas compris ta question.
Mon AuthenticationService est un objet normal de type Zend\Authentication\AuthenticationService. Sauf que je lui ait donné un adapter perso qui va chercher l'utilisateur en base.
En session je ne stocke que l'identifiant, et quand je fais un $authenticationService->getIdentity(), il ira chercher dans la base de données l'utilisateur correspondant, l'objet retourné implémentant l'interface RoleInterface.
Donc si aucun utilisateur n'est loggué, getIdentity retournera null et le rôle sera 'guest'.
Hors ligne
En fait la confusion vient du fait que tu as un adapter perso .
En session je ne stocke que l'identifiant mais lorsque je fais $authenticationService->getIdentity() il me renvoi uniquement l'id et c'est à moi ensuite d'aller récupérer l'utilisateur en base. Du coup j'ai "compris" que tu stockais le role en session et ça me paraissait un peu incohérent ^^.
Je pense que ça pourrait être intéressant que tu complètes ton article justement en abordant cette partie là. Détailler un peu plus comment tu récupères l'utilisateur et l'utilisation d'une classe implémentant l'interface RoleInterface. De cette façon on pourrait bien percevoir le cheminement complet : de la récupération en base jusqu'à l'accès à la page.
Hors ligne
Ok, je te mets ça si j'oublie pas en rentrant ce soir .
Hors ligne
Je crois que tu as oublié :p !
Hors ligne
Effectivement ^^.
Voici l'objet DbStorage :
namespace Common\Authentication\Storage; use Doctrine\ORM\EntityManager, Zend\Authentication\Storage\StorageInterface; /** * Cette classe implémente l'interface StorageInterface et permet de stocker le résultat d'une authentification dans la base de données */ class Db implements StorageInterface { /** * @var \Doctrine\ORM\EntityManager */ protected $em; /** * @var \Zend\Authentication\Storage\StorageInterface */ protected $storage; /** * @var string */ protected $identityClassName; /** * @param \Doctrine\ORM\EntityManager $em * @param \Zend\Authentication\Storage\StorageInterface $storage * @param $identityClassName */ public function __construct(EntityManager $em, StorageInterface $storage, $identityClassName) { $this->em = $em; $this->storage = $storage; $this->identityClassName = $identityClassName; } /** * @return bool */ public function isEmpty() { return $this->storage->isEmpty(); } /** * @return mixed|null|object */ public function read() { $identity = $this->storage->read(); if (is_int($identity) || is_scalar($identity)) { $identity = $this->em->getRepository($this->identityClassName) ->find($identity); } else { $identity = null; } return $identity; } /** * @param $contents */ public function write($contents) { $this->storage->write($contents); } /** * */ public function clear() { $this->storage->clear(); } }
Il s'agit ni plus ni moins d'une implémentation de l'interface StorageInterface. Cette implémentation contient elle même un autre StorageInterface (dans mon cas, une session toute bête, contenant uniquement l'identifiant).
Hors ligne
Merci bien
Hors ligne
Salut Bakura,
J'avais décidé moi aussi d'implémenter mon propre système mais comme il existe déjà le module "officiel" ZfcAcl et le module ZfcUserAcl qui va lui aussi être prochainement migré dans ZF-Commons, je pense qu'il est préférable de faire de l'intégration de modules ZfcUser,ZfcUserDoctrineORM,ZfcAcl et ZfcUserAcl.
https://github.com/ZF-Commons/ZfcAcl
https://github.com/bjyoungblood/ZfcUserAcl
Quoiqu'il en soit bravo pour tout boulot et surtout d'avoir pris le temps de nous le partager.
Merci!!
Hors ligne
Bonsoir,
Il existe aussi un module pour utiliser un système RBAC au lieu des ACLs.
J'ai commencé à jouer avec et il est pas mal, même si perfectible.
https://github.com/ZF-Commons/ZfcRbac
Hors ligne
Salut,
Je sais, j'y ait contribué à ZfcRbac . Mon code n'est plus valide de toute façon.
Hors ligne
J'ai vu
et j'ai moi aussi lancé plusieurs pull requests, mais le maintainer n'est pas hyper réactif...
Je dois t'avouer que ça nous inquiète un peu. Ca fait deux mois que Spiffy ne s'est pas connecté sur aucun channel IRC. On lui a envoyé un mail pour savoir ce qu'il faisait, car c'est un peu bizarre (ok, il a eu un bébé et il a un boulot assez chronophage mais il était quand même quasiment tout le temps là sur IRC, donc là c'est un peu étrange...).
Hors ligne
Ca dépend... En l'occurrence son répo est sur ZF-Commons, donc au pire si on a pas de nouvelles de lui d'ici 1 ou 2 mois (auquel cas ça m'ennuierais un peu, ça voudrait dire qu'il s'est passé un truc pas cool ), je demanderai à Matthew qu'il me donne l'accès.
Si les Repo avaient été sur son compte perso bah... on aurait du le forker et faire du fork le "nouveau" répo officiel.
Hors ligne
OK, et ben je vais continuer à guetter ce projet et essayer de me mettre à IRC.
Puisqu'on en parle, est-ce qu'une demande de tag à sa place dans les "issues" de github ?
En effet, pour pouvoir utiliser un module en prod, il faudrait que sa release soit tagguée afin de pouvoir avoir des sources stables.
Hors ligne
Oui. Ca aurait sa place .
Hors ligne
Je viens de tomber sur BjyAuthorize
Ca m'a l'air pas mal du tout et on dirait qu'il a intégré les fonctionnalités de ZfcRbac.
Bizarre que ce ne soit pas intégré aux modules communs Zfc, surtout qu'il annule et remplace ZfcAcl.
Hors ligne
ZfcAcl n'est plus supporté. Il y a eu quelques soucis avec Zfc, c'est un truc assez rigide et finalement je crois qu'il n'y aura plus de gros modules intégrés à Zfc.
Hors ligne
Bah BjyAuthorize est basé sur Zend\Permissions\Acl, ZfcRbac est basé sur Zend\Permissions\Rbac.
Niveau code, Zend\Permissions\Rbac a été écrit après et est donc plus "moderne" (il utilise plusieurs fonctionnalités de PHP 5.3), après c'est une différence de modèle surtout. Faut que tu voie, suivant ton application, quel modèle est le mieux adapté.
Hors ligne
Merci pour ce code. Même s'il n'est plus valide actuellement les idées sont la.
Petite question, vous avez l'aire de connaitre un peut BjyAuthorize. Faut il absolument utiliser ZfcUser pour pouvoir l'utiliser? (et donc la même structure de base de donnée ....)
Merci d'avance
Hors ligne
Non je n'ai jamais utilisé BjyAuthorize mais j'en ait eu de bons retours.
Hors ligne
Ca serait bien un petit cookbook de zfcrbac, car là je n'arrive pas à l'utiliser en l'état.
J'ai regardé le module BjyAuthorize , ce module propose le schema pour la table user_role, et il requiert le module zfcuser. Zfcrbac ne propose rien, c'est à nous de d'implémenter le reste ?
Merci
Hors ligne