Atelier Zend Framework : Autorisations : lier les ACLs aux contrôleurs et objets métiers
Date de publication : 19/03/2009 , Date de mise à jour : 30/07/2009
Par
Julien Pauli (Tutoriels, article et conférences PHP et developpement web) (Blog)
Les ACLs sont souvent un casse-tête à gérer, du moins si on ne prend pas la peine de réfléchir à créer sa propre structure de gestion de celles-ci.
Souvent dans nos applications, c'est toujours la même chose. Ici je vais présenter un système de gestion automatisé des ACLs.
Attention comme tout atelier : il donne une idée que j'ai et que j'utilise sur le coup, mais qui n'est pas forcément adapté à tous, et possède un certain degré
de couplage qui pourra peut-être déplaire. Ca donne en revanche quelques idées de manipulations avec le Zend Framework.
23 commentaires ·
I. Introduction
II. Créer son objet ACL
III. Lier son métier à ses ACLs
IV. Gérer les ressources
V. Automatiser le processus d'interrogation des ACLs
VI. Conclusions
I. Introduction
#Zend_Acl est un composant servant à lier des rôles (en général "qui?") à des ressources (en général "pour qui/quoi?"), via des droits ( en général "quoi?").
En fait on répond à la question "qui peut faire quoi avec qui/quoi?". Identifier les ressources, les rôles, et les droits... Tel est le problème.
La réponse est : tout dépend de votre application :-).
Mes choix pour cet atelier ont été les suivants :
- Le rôle est un objet métier issu de Zend_Db_Table_Row
- La ressource est un contrôleur issu de Zend_Controller_Action
- Le droit correspond à une action du contrôleur
Evidemment, libre à vous de l'adapter à vos propres objets, avec votre propre structure.
II. Créer son objet ACL
Nous allons ici dériver la classe Zend_Acl. Les buts sont multiples :
- Faire en sorte que l'objet ACL persiste en session de lui-même
- Ajouter un rôle de plus bas niveau (rôle par défaut) contenant les droits miniums : "guest"
Créer l'objet ACL est facile, mais encore faut-il le faire persister entre chaque requête HTTP de manière à éviter de le recréer à chaque requête, et aussi de manière à pouvoir le modifier, le peupler de nouveaux droits,
et faire persister ces nouveaux droits entre chaque requête.
Plusieurs systèmes de persistances peuvent être choisis, et une interface permettrait de découpler le tout. La solution retenue dans cet atelier sera cependant imposée : la session.
Notre nouvel objet ACL sera un singleton. L'appel au singleton persistera automatiquement en session, dans un Zend_Session_Namespace configurable. Celui-ci devra pouvoir être remis à zéro,
et évidemment tout changement de droit doit être reflété dans l'objet en session, de manière là aussi automatique.
Attention : veillez à ce que la création du namespace de session n'influe pas sur la session, vous devrez peut-être la démarrer manuellement si son options 'strict' est à 'on'. Je vous renvoie au manuel pour plus d'infos.
Vous allez voir que nous allons être joliment concernés par le
Late Static Binding, introduit dans PHP5.3 (cet article concerne donc PHP5.2)
Nous allons devoir utiliser une gymnastique d'héritage du contexte statique dans PHP 5.2, qui va nous obliger à dupliquer un bout de code. Ce problème n'apparait pas dans PHP5.3.
<?php
class JP_Acl extends Zend_Acl
{
@varstring
protected static $_defaultRole = 'guest';
@varstring
protected static $_sessionNamespace = 'acl';
@var
protected static $_instance;
@var
protected static $_session;
@vararray$params
public static function setParams(array $params)
{
if (isset($params['defaultRole'])) {
self::$_defaultRole = $params['defaultRole'];
}
if (isset($params['sessionNamespace'])) {
self::$_sessionNamespace = $params['sessionNamespace'];
}
}
@parambool$clearSession
@return
public static function getInstance($clearSession = false)
{
function getInstance()
{
if (self::$_instance == null) {
self::$_instance = new self;
}
return self::$_instance;
}
if (self::$_sessionNamespace !== null) {
if ($clearSession) {
Zend_Session::namespaceUnset(self::$_sessionNamespace);
}
self::$_session = new Zend_Session_Namespace(self::$_sessionNamespace);
if (isset(self::$_session->acl)) {
self::$_instance = self::$_session->acl;
} else {
self::$_instance = getInstance();
}
} else {
self::$_instance = getInstance();
}
return self::$_instance;
}
}
|
Tout singleton possède une méthode statique getInstance() (ou dans le même style), permettant de créer l'instance de la classe elle-même.
La différence ici, c'est que l'instance doit pouvoir être récupérée de la session si elle s'y trouve, ou être stockée en session dans le cas contraire. Le tout si le mécanisme de persistance est activé (si $_sessionNamespace est non null).
Pour régler la persistance, il faut utiliser la méthode statique setParams()
Tout singleton déclare un constructeur non public. Le notre doit pouvoir être hérité, il sera donc protégé, mais il sera aussi déclaré final : le constructeur doit ajouter un rôle minimum par défaut (guest par défaut), obligatoirement.
Nous allons donc couper le constructeur en empêchant sa redéfinition, mais en permettant son extension, via un simple proxy de méthode :
| Suite de JP_Acl |
<?php
final protected function __construct()
{
$this->addRole(new Zend_Acl_Role($this->getDefaultRole()));
$this->_init();
}
protected function _init() { }
@returnstring
public function getDefaultRole()
{
return self::$_defaultRole;
}
|
On notera qu'à ce stade, si l'objet est crée de manière vierge (et non depuis la session), il n'y est pas sauvegardé. Nous allons faire d'une pierre deux coups :
Comme à chaque modification de l'objet il faut le refaire persister en session de manière automatique, nous allons redéfinir allow() et deny(), et comme notre constructeur
utilise une de ses méthodes, l'objet sera sauvé en session dès sa construction depuis un état vierge :
| sauvegarde en session de l'objet s'il est modifié |
<?php
public function allow($roles = null, $resources = null, $privileges = null, Zend_Acl_Assert_Interface $assert = null)
{
$return = parent::allow($roles, $resources, $privileges, $assert);
if (self::$_session !== null) {
self::$_session->acl = $this;
}
return $return;
}
public function deny($roles = null, $resources = null, $privileges = null, Zend_Acl_Assert_Interface $assert = null)
{
$return = parent::deny($roles, $resources, $privileges, $assert);
if (self::$_session !== null) {
self::$_session->acl = $this;
}
return $return;
}
|
Notons que le sous-espace de nom de session "acl" n'est pas modifiable ici.
Voila, notre objet est terminé, il est prêt à être redéfini mais vu qu'il peut servir comme objet "de base", vierge, nous ne passerons pas sa classe en abstrait.
Voyons comment dériver l'objet :
| My_Acl pour mon application spécifique |
<?php
class MyAcl extends JP_Acl
{
@parambool$clearSession
@return
public static function getInstance($clearSession = false)
{
function getInstance()
{
if (self::$_instance == null) {
self::$_instance = new self;
}
return self::$_instance;
}
if (self::$_sessionNamespace !== null) {
if ($clearSession) {
Zend_Session::namespaceUnset(self::$_sessionNamespace);
}
self::$_session = new Zend_Session_Namespace(self::$_sessionNamespace);
if (isset(self::$_session->acl)) {
self::$_instance = self::$_session->acl;
} else {
self::$_instance = getInstance();
}
} else {
self::$_instance = getInstance();
}
return self::$_instance;
}
protected function _init()
{
$this->add(new Zend_Acl_Resource('livres'))
->addRole(new Zend_Acl_Role('libraire'), 'guest')
->addRole(new Zend_Acl_Role('admin'));
$this->allow('admin');
$this->allow('libraire', 'livres', 'add');
$this->allow('guest','livres', 'liste');
}
}
|
Le problème du LSB est très clair dans le cas de l'héritage d'un singleton : dans le code "new self;" , "self" représente la classe dans laquelle le mot "self" lui-même est écrit.
En d'autres termes, "new self" n'est pas hérité, et "self" représente JP_Acl si on ne réécrit pas TOUT le code de getInstance() dans la classe fille (ici MyAcl)
Ce problème connu, qui gène les conceptions objets avancées, est résolu dans PHP5.3 avec l'apparition du mot "static" pour la syntaxe "static::$quelqueChose", ou encore "new static". "static" est ici un "self hérité", dont
la portée est résolue tardivement ("Late Static Binding").
Pour conclure, si vous ne réécrivez pas toute la méthode getInstance(), vous obtiendrez un objet JP_Acl père, et votre méthode _init() fille sera ignorée : tout tombe à l'eau.
La méthode init(), ici, gère les ACLs de MyAcl : mes ACLs pour mon application d'exemple. Mon objet ACL hérite donc de tout le mécanisme de persistance en session implémenté dans JP_Acl.
Une ressource "livres", qui on le verra plus tard interragira avec un "LivresController". Un rôle libraire héritant du rôle par défaut définit en amont dans JP_Acl, enfin un rôle admin.
- L'admin à le droit de tout faire
- Le libraire peut ajouter (add) des livres
- L'invité, donc le libraire qui en hérite, peut lister les livres
Chacun des droits ('liste' et 'add') correspondra plus tard à des méthodes addAction() et listeAction() du contrôleur Livres.
III. Lier son métier à ses ACLs
Ok, j'ai des rôles. Voyons comment les gérer facilement. J'ai décidé quelque chose de simple :
- Je possède une table de membres
- La table possède une colonne "rôle" qui définit le même type de rôle que ceux des ACLs
- Mes objets Zend_Db_Table_Row de membres doivent pouvoir donner leur rôle
| Définition de JP_Db_Table_Row : classe support des objets identifiables via les ACLs |
<?php
abstract class JP_Db_Table_Row extends Zend_Db_Table_Row_Abstract implements Zend_Acl_Role_Interface
{
@var
protected static $_acl;
public static function setAcl(JP_Acl $acl)
{
self::$_acl = $acl;
}
@returnstring
protected function _getRoleKey() { }
@returnstring
public function getRoleId()
{
if ($this->_getRoleKey() === null) {
return self::$_acl->getDefaultRole();
}
return $this->_getRoleKey();
}
}
|
<?php
class Membres extends JP_Db_Table_Row
{
@varint$sessionDuration
@return
public function authenticate(sessionDuration = 120)
{
$db = $this->getTable()->getAdapter();
$table = $this->getTable()->info('name');
$adapter = new Zend_Auth_Adapter_DbTable($db, $table, 'nom', 'password');
$adapter->setIdentity($this->nom);
$adapter->setCredential($this->password);
$result = $adapter->authenticate();
if ($result->isValid()) {
$this->setFromArray((array)$adapter->getResultRowObject());
$session = new Zend_Session_Namespace('Zend_Auth');
$session->setExpirationSeconds(abs((int)$sessionDuration));
Zend_Auth::getInstance()->getStorage()->write($this);
}
return $result;
}
public function _getRoleKey()
{
return $this->role;
}
}
|
Cette classe JP_Db_Table_Row est facilement injectable dans la table supposée Membres :
| Injection du support d'objet Row à la table Membres |
<?php
class TMembres extends Zend_Db_Table_Abstract
{
protected $_rowClass = 'Membres';
}
|
Ce framework est décidément très souple!
Notez la méthode authenticate() qui permet d'authentifier une personne depuis la table Membres, et d'éventuellement faire persister l'identité en session.
Voila, je vais pouvoir interroger mon ACL avec un objet Membres_Db_Table_Row issu de la table des Membres, car celui-ci implémente bien Zend_Acl_Role_Interface
| exemple simple d'utilisation |
<?php
$acl = MyAcl::getInstance();
JP_Db_Table_Row::setAcl($acl);
$TMembres = new TMembres();
$membre = $TMembres->createRow();
$membre->login = 'developpez';
$membre->password = 'secret';
$result = $membre->authenticate(1800);
if (!$result->isValid()) {
exit('login ou mot de passe incorrect');
}
if ($acl->isAllowed($membre, 'une ressource, par exemple "livres"', 'un doit, par exemple "liste"')) {
echo "Ok autorisé";
} else {
echo "KO non autorisé";
}
|
Voyons maintenant comment gérer les ressources.
IV. Gérer les ressources
J'ai indiqué plus tôt que pour cet atelier, tout contrôleur est une ressource. Tout contrôleur va donc implémenter Zend_Acl_Resource_Interface, il convient
donc de créer l'étage (la classe abstraite) convenable pour cela :
| Contrôleurs ressources génériques |
<?php
class JP_Controller_AclAction extends Zend_Controller_Action implements Zend_Acl_Resource_Interface
{
@varstring
protected $_resourceId;
@returnstring
public function getResourceId()
{
return $this->_resourceId;
}
public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
{
$this->_resourceId = strtolower($request->getControllerName());
parent::__construct($request, $response, $invokeArgs);
}
|
Le nom du futur contrôleur qui héritera de cette classe sera considéré comme un ressource. Ainsi, LivresController sera la ressource 'livres'.
Elle devra exister dans les ACLs qu'il faudra préparer auparavant, dans le cas où la ressource n'existe pas, on verra que l'on choisira d'interdir l'accès plutôt que de générer une erreur.
Nous verrons aussi dans la partie suivante que le droit demandé correspondra à l'action. Un contrôleur de livres pourra ressembler à ceci:
| Un contrôleur 'livres' |
<?php
class LivresController extends JP_Controller_AclAction
{
public function listeAction()
{
}
public function addAction()
{
}
}
|
V. Automatiser le processus d'interrogation des ACLs
A ce stade, j'ai un objet JP_Acl générique qui s'auto-stocke en session et ajoute un rôle par défaut appelé 'guest'.
Un objet MyAcl spécifique à mon application d'exemple (héritant de JP_Acl), qui ajoute 3 rôles et une ressource.
J'ai aussi des rôles qui sont liés à une table Membres et notamment à une colonne 'role' dans celle-ci, interrogée via un objet de table (TMembres) et un objet métier (Membres_Db_Table_Row). L'objet métier
possède une méthode authenticate() qui identifie le membre représenté vis à vis de sa table, et fait persister son identitié en session un certain temps. Les objets métiers membres héritent d'un conteneur plus générique
gèrant les rôles des objets métiers en héritant (JP_Db_Table_Row).
Enfin j'ai des contrôleurs génériques JP_Controller_AclAction qui représentent dans les acls des ressources determinées via le nom des contrôleurs spécifiques en héritant (LivresController par exemple)
Il manque une pièce au puzzle : automatiser l'interrogation des acls pour tous les contrôleurs traités. Comme nous l'avons vus dans
cet atelier
rien n'est plus approprié pour cela qu'une aide d'action. En effet, le contrôleur en cours de traitement devra être connu, et seules les aides d'action peuvent faire cela.
| L'aide d'action qui automatise le processus de vérification des ACLs |
<?php
class JP_Controller_Action_Helper_Acl extends Zend_Controller_Action_Helper_Abstract
{
@var
protected $_acl;
public function __construct(JP_Acl $acl = null)
{
$this->_acl = $acl;
}
@varbool
protected $_disabled = false;
public function init()
{
if ($this->_acl == null) {
if (!$this->getFrontController()->getParam('acl') instanceof JP_Acl) {
throw new Zend_Controller_Action_Exception('acl object not shared');
}
$this->_acl = $this->_frontController->getParam('acl');
}
}
public function preDispatch()
{
if (!$this->_actionController instanceof Zend_Acl_Resource_Interface ||
$this->_disabled) {
return;
}
$action = $this->_actionController->getRequest()->getActionName();
try {
$result = $this->_acl->isAllowed($this->_getActualRole(), $this->_actionController, $action);
} catch (Zend_Acl_Exception $e) {
$result = false;
}
if ($result === false) {
throw new ForbiddenException();
}
}
protected function _getActualRole()
{
$auth = Zend_Auth::getInstance();
if (!$auth->hasIdentity()) {
return $this->_acl->getDefaultRole();
}
return $auth->getIdentity()->getRoleId();
}
public function disable()
{
$this->_disabled = true;
}
}
|
Cette aide d'action a besoin de mon objet acl. Soit je le lui passe en construction, soit je le propage via le contrôleur frontal, l'aide d'action essayant alors de le récupérer.
En preDispatch(), donc avant toute action traitée, on vérifie que le contrôleur est bien utilisable directement avec l'objet acl. Si ce n'est pas le cas, par défaut on choisit
d'ignorer tout le processus des acls et donc d'ouvrir l'accès. Une petite refactorisation permet ici de faire un choix.
Aussi, chaque contrôleur pourra choisir de désactiver temporairement le contrôle des acls pour lui-même. Nous allons voir un exemple.
Ensuite on interroge l'objet acl : le rôle est issu de la session, sinon le rôle par défaut est utilisé. La ressource est bien le contrôleur lui-même, et le droit à vérifier est bien l'action
qu'on tente de lancer.
| utilisation de l'aide d'action |
<?php
$acl = MyAcl::getInstance();
Zend_Controller_Action_HelperBroker::addHelper(new JP_Controller_Action_Helper_Acl($acl));
Zend_Controller_Action_HelperBroker::addHelper(new JP_Controller_Action_Helper_Acl());
$frontController->setParam('acl', $acl);
|
Et voilà :-)
Encore une chose, si un contrôleur veut désactiver le processus des acls, il n'a qu'à l'indiquer à l'aide d'action. On voit bien la facilité de communication entre les actions et leurs aides, mutuellement :
| désactivation temporaire de la vérification des acls pour ce contrôleur complet |
<?php
class LivresController extends JP_Controller_AclAction
{
public function init()
{
$this->_helper->acl->disable();
}
}
|
VI. Conclusions
Comme nous vennons de le voir, la fléxibilité du ZendFramework offre une fois de plus la possibilité de monter des structures très intéréssantes. Pourquoi ceci n'est-il pas implémenté de base dans le framework ? Car ZendFramework propose, et n'impose pas.
Les idées, c'est à chacun de les trouver, en analysant et en comprenant les objets du ZendFramework.
Cette idée d'ACL en raviera certainement plus d'un, on peut lui trouver quelques inconvénients, libres à vous alors de la modifier ;-)


Copyright © 2009 Julien Pauli. Aucune reproduction, même partielle, ne peut être faite
de ce site et de l'ensemble de son contenu : textes, documents, images, etc
sans l'autorisation expresse de l'auteur.
Sinon vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E
de dommages et intérêts.
Cette page est déposée à la
SACD.