« Return to Thread: Zend_Acl / Zend_Auth example scenario

Re: Zend_Acl / Zend_Auth example scenario

by Simon Mundy :: Rate this Message:

Reply to Author | View in Thread

It may well have changed - I've been using Ralph's proof of concept  
from 2 weeks ago which did indeed use a singleton pattern.

> In the boostrap, you  are using "$auth = Zend_Auth::getInstance
> ();", but as far as i know, zend_Auth does not implement the  
> singleton design pattern, no ?
> Have you made such implementation on your own ?
>> Hi there all
>>
>> After submitting the initial example of how Zend_Auth and Zend_Acl  
>> could be implemented Gavin pointed out areas that weren't really  
>> addressed in my proof of concept and it could potentially confuse  
>> newcomers to the way MVC is utilised. I'd like to clarify that  
>> post to a) Address those concerns and b) see if there's any  
>> constructive criticism of the process that could benefit everyone.
>>
>>
>> *Requirements:*
>> -------------
>> Demonstrate a web environment where 'public' (i.e. non-
>> authenticated) users and 'member' users have access restrictions,  
>> and to what context they may visit those resources. In a lot of  
>> ways this broad concept relates very well to small-medium sites of  
>> a lot of Zend developers (in my opinion). For purpose of clarity  
>> we will assume this is a SIG group for Mac Users to discuss all  
>> things Mac OS X-related. The site has 3 areas (home, news,  
>> tutorials) that are for the general public. Members can also view  
>> a discussion forum, community newsletter and support request area  
>> for members to share common problems.
>>
>> *Site layout*
>> -------------
>> Expressed as :controller/:action notation:-
>>
>> /home
>>
>> /news/index
>>      /view
>>      /email
>>
>> /tutorials/index
>>           /view
>>
>> /forum/index
>>       /category
>>       /view
>>       /add
>>       /update
>>       /reply
>>       /search
>>       /report - report abuse, etc.
>>
>> /support/index
>>         /view
>>         /search
>>         /submit
>>         /confirmation -         /comment - add comment
>>
>> /login/index - handles form processing and auth processing
>>
>> /logout/index - destroys current auth instance
>>
>> /error/noroute - handles all 404s
>>       /failure - handles 'Site error' messages
>>       /privileges - handles 'You are not privileged...' messages
>>
>> /admin - a cms to handle all site management
>>
>> This loosely illustrates the site functionality and content - for  
>> the sake of brevity we'll assume that the general concepts and  
>> operations of these site functions are understood and familiar.  
>> What we're interested in is how to handle user authentication and  
>> then access, but at least this gives us some 'real world'  
>> understanding of what is required.
>>
>>
>> *Access rules:*
>> -------------
>> Three types of user 'roles' have been identified for the site:-
>>
>> *guest* (not authenticated) - Guests can access 'home', 'news' and  
>> 'tutorials' only. Guests attempting to access member-only content  
>> will be asked to authenticate.
>>
>> *member* (authenticated) - Access all top-level controllers. Can  
>> update forum posts but only those authored by themselves. Not  
>> allowed access to admin section. Access to 'admin' will result in  
>> 'privileges' error message.
>>
>> *admin* (authenticated) - Unrestricted access.
>>
>>
>> *Application layout:*
>> -------------------
>> Using the 'Conventional' layout that Gavin outlines in http://
>> framework.zend.com/wiki/display/ZFDEV/Choosing+Your+Application%27s
>> +Directory+Layout
>>
>> The bootstrap is located inside /htdocs/index.php
>>
>>
>> *Bootstrap:*
>> ----------
>> The bootstrap takes care of the usual suspects - Db, View, Config,  
>> Log, Router - and stores them inside the Zend_Front_Controller so  
>> that they can be accessed via each controller using the  
>> getInvokeArg() method. This negates the need for an extra registry  
>> object and (hopefully) makes the dependencies somewhat easier to  
>> track.
>>
>> To satisfy the needs of the Access rules, we create a subclassed  
>> instance of Zend_Acl like so:-
>>
>> class MyAcl extends Zend_Acl
>> {
>>     public function __construct(Zend_Auth $auth)
>>     {
>>         parent::__construct();
>>
>>         $roleGuest = new Zend_Acl_Role('guest');
>>
>> $this->add(new Zend_Acl_Resource('home'));
>> $this->add(new Zend_Acl_Resource('news'));
>> $this->add(new Zend_Acl_Resource('tutorials'));
>> $this->add(new Zend_Acl_Resource('forum'));
>> $this->add(new Zend_Acl_Resource('support'));
>> $this->add(new Zend_Acl_Resource('admin'));
>>
>>         $this->addRole($roleGuest);
>>         $this->addRole(new Zend_Acl_Role('member'), 'guest');
>>         $this->addRole(new Zend_Acl_Role('admin'), 'member');
>>
>>         // Guest may only view content
>>         $this->allow('guest', 'home');
>>         $this->allow('guest', 'news');
>>         $this->allow('guest', 'tutorials');
>>         $this->allow('member', 'forum');
>>         $this->deny('member', 'forum', 'update'); // Remove  
>> specific privilege
>>         $this->allow('member', 'support');
>>         $this->allow('admin'); // unrestricted access
>>
>>         // Add authoring ACL check
>>         $this->allow('member', 'forum', 'update', new  
>> MyAcl_Forum_Assertion($auth));
>>         // NOTE: Dependency on auth object to allow getIdentity()  
>> for authenticated user object
>>     }
>> }
>>
>> ...and then this is added to the bootstrap. The final index.php  
>> file looks something like:-
>>
>>
>> *Index.php*
>> <?php
>>
>> // Initialise configuration / environment
>> $config = new Zend_Config(new Zend_Config_Ini('../application/
>> config/config.ini', 'live'));
>>
>> // Create sitemap from .ini using structure from example
>> $sitemap = new Zend_Config(new Zend_Config_Ini('../application/
>> config/sitemap.ini', 'live'));
>>
>> // Create db object and enable/disable debugging
>> $db = Zend_Db::factory($config->db->connection, $config->db-
>> >asArray());
>> ...etc...
>>
>> // Create auth object
>> $auth = Zend_Auth::getInstance();
>>
>> // Create acl object
>> $acl = new MyAcl($auth); // see
>> // Create router and configure (LIFO order for routes)
>> $router = new Zend_Controller_RewriteRouter;
>> ...add rules...
>>
>> // Create view and register objects
>> $view = new My_View;
>> ...init view...
>>
>> $front = Zend_Controller_Front::getInstance();
>> $front->throwExceptions(true);
>> $front->setRouter($router)
>>       ->setDispatcher(new Zend_Controller_ModuleDispatcher())
>>       ->registerPlugin(new My_Plugin_Auth($auth, $acl))
>>       ->registerPlugin(new My_Plugin_Agreement($auth))
>>       ->registerPlugin(new My_Plugin_View($view))
>>       ->setControllerDirectory(array('default' => realpath('../
>> application/controllers/default'),
>>                                      'admin' => realpath('../
>> application/controllers/admin')))
>>       ->setParam('auth', $auth)
>>       ->setParam('view', $view)
>>       ->setParam('config', $config)
>>       ->setParam('sitemap', $sitemap)
>>       ->dispatch();
>>
>>
>> This is a pretty standard (IMO) bootstrap - the areas to note for  
>> the purpose of Authentication/Acl are the two first plugins:-
>>
>> *Auth.php*
>> The purpose of this plugin is to first determine the 'role' of the  
>> current Auth identity. If Zend_Auth::getIdentity() returns false  
>> then we don't have a 'role' for the identity, so we assume  
>> 'guest'. If a user is authenticated, the Zend_Auth identity would  
>> be returned as an object and we would extract the role from this.  
>> For simplicity's sake, let's assume that the 'role' is stored in a  
>> MySQL database and is returned as a public property from the  
>> Identity object (i.e. 'member' or 'admin').
>>
>> The 'role' is then a one-to-one match against the Acl rules. If we  
>> interrogate the Acl and we are allowed to view the current  
>> controller (maps to the 'resource' id given to each Acl resource)  
>> then the dispatcher continues on its merry way.
>>
>> If the Acl denies the access, we then determine if the user has a  
>> valid identity. If not, we tell the request object that we want to  
>> redirect to a new controller (login) to perform a login. *At this  
>> stage, no request data is required - this will be handled via a  
>> form in the LoginController*.
>>
>> If, however, the identity is valid then we know that access if  
>> definitely blocked for that user and we send the request to the  
>> 'error' controller to display the 'no privleges' error.
>>
>> I've chosen this strategy as it means that none of the controllers  
>> need know anything about the ACL process - they can assume that  
>> access to the action has been already approved and need only check  
>> action-specific privilege checks (e.g. ensuring they view valid  
>> articles, forum threads, etc.)
>>
>> However a developer could still choose to add further ACL rules if  
>> required and reduce the amount of ACL-related 'clutter' in the  
>> controllers themselves.
>>
>> <?php
>>
>> class My_Plugin_Auth extends Zend_Controller_Plugin_Abstract
>> {
>>     private $_auth;
>>     private $_acl;
>>
>>     private $_noauth = array('module' => 'default',
>>                              'controller' => 'login',
>>                              'action' => 'index');
>>
>>     private $_noacl = array('module' => 'default',
>>                             'controller' => 'error',
>>                             'action' => 'privileges');
>>        public function __construct($auth, $acl)
>>     {
>>         $this->_auth = $auth;
>>         $this->_acl = $acl;
>>     }
>>
>> public function preDispatch($request)
>> {
>>         if ($this->_auth->hasIdentity()) {
>>             $role = $this->_auth->getIdentity()->getUser()->role;
>>         } else {
>>             $role = 'guest';
>>         }
>>            $controller = $request->controller;
>>     $action = $request->action;
>>     $module = $request->module;
>> $resource = $controller;
>>        if (!$this->_acl->has($resource)) {
>>         $resource = null;
>>     }
>>
>>         if (!$this->_acl->isAllowed($role, $resource, $action)) {
>>             if (!$this->_auth->hasIdentity()) {
>>                 $module = $this->_noauth['module'];
>>                 $controller = $this->_noauth['controller'];
>>                 $action = $this->_noauth['action'];
>>             } else {
>>                 $module = $this->_noacl['module'];
>>                 $controller = $this->_noacl['controller'];
>>                 $action = $this->_noacl['action'];
>>             }
>>         }
>>
>>         $request->setModuleName($module);
>>         $request->setControllerName($controller);
>>         $request->setActionName($action);
>> }
>> }
>>
>>
>> *Agreement.php*
>> Many sites choose to enforce a set of terms and conditions to  
>> access. This intercepting plugin simply checks the Zend_Auth  
>> identity method hasAgreement() (for the sake of demonstration lets  
>> just say this is a boolean property that has been set in the user  
>> table of the database). Again, this is only enacted if an identity  
>> exists, and the request is redirected to a specific agreement  
>> controller/action.
>>
>> <?php
>>
>> class MyPlugin_Agreement extends Zend_Controller_Plugin_Abstract
>> {
>>     private $_auth;
>>
>>     private $_noagreement = array('module' => 'default',
>>                                   'controller' => 'login',
>>                                   'action' => 'agreement');
>>        public function __construct($auth)
>>     {
>>         $this->_auth = $auth;
>>     }
>>
>> public function preDispatch($request)
>> {
>>         if ($request->controller != 'logout' && $this->_auth-
>> >hasIdentity()) {
>>             if (!$this->_auth->getIdentity()->getUser()-
>> >hasAgreement()) {
>>                 $request->setModuleName($this->_noagreement
>> ['module']);
>>                 $request->setControllerName($this->_noagreement
>> ['controller']);
>>                 $request->setActionName($this->_noagreement
>> ['action']);
>>             }
>>         }
>> }
>> }
>>
>>
>> *Login (Authentication)*
>> The act of authentication - in my app - all happens within a  
>> domain model - MyForm_Login. Using Matt Zandstra's excellent  
>> reference on Observers/Observable at zend.com as a starting point  
>> I have created a form object that extends the PEAR HTML_Quickform  
>> component to allow one or more observers to be added to the form  
>> and activated upon validation.
>>
>> The form is constructed (and auto-populated in my domain-specific  
>> instance with form elements like 'Username', 'Password' and a  
>> 'Remember Me' checkbox), then several observers are added to it.
>>
>> When a form validates, the observers are all notified and given an  
>> instance of the form values and the Zend_Auth instance. From  
>> there, it is simply a matter of checking the /sanitised/ form  
>> values (we've applied our form filters, right? :) and passing them  
>> to a domain-specific Zend_Auth_Identity object to query the  
>> database, perform a lookup and then either spit out an error  
>> message or start the login session.
>>
>> The example below would also create a hypothetical log observer to  
>> record the login time, date, details, etc.
>>
>> BTW, in case you're wondering why the $view->render() isn't  
>> called, it's because I generally have a View_Plugin that's  
>> registered in the the Front_Controller that kicks in during  
>> dispatch shutdown. It allows me to incrementally add components/
>> properties to the view as the dispatcher loops through all the  
>> application actions.
>>
>>
>> *LoginController.php*
>> class LoginController extends Zend_Controller_Action
>> {
>> public function indexAction()
>> {
>>     $auth = $this->getInvokeArg('auth');
>>     $view = $this->getInvokeArg('view');
>>
>>     if ($auth->hasIdentity()) {
>>         $this->_redirect('/home/index'); // Already authenticated?  
>> Navigate away
>>     }
>>
>>         $form = new MyForm_Login(); // creates all fields, adds  
>> filters, etc...
>>         $form->attach(new MyPlugin_Login_User($auth); // Perform  
>> login of user identity
>>         $form->attach(new MyPlugin_Login_Log($auth); // Perhaps  
>> log the event?
>>
>>         if ($form->validate()) {
>>         $this->_redirect('/home/index');
>>         }
>>         // Render page
>>         $this->getInvokeArg('view')->title = 'Login';
>>         $this->getInvokeArg('view')->template = 'login/index.tpl';
>>         $this->getInvokeArg('view')->form = $form->render();
>> }
>> public function agreementAction()
>> {
>>     $auth = $this->getInvokeArg('auth');
>>     $view = $this->getInvokeArg('view');
>>
>>         $form = new MyForm_Agreement();
>>         $form->attach(new MyPlugin_Agreement_User($auth));
>>
>>         if ($form->validate()) {
>>         $this->_redirect('/home/index');
>>         }
>>         // Render page
>>         $this->getInvokeArg('view')->title = 'Agreement';
>>         $this->getInvokeArg('view')->template = 'login/
>> agreement.tpl';
>>         $this->getInvokeArg('view')->form = $form->render();
>> }
>> }
>>
>>
>> *User.php*
>> class MyPlugin_Login_User implements Observer
>> {
>>     function notify($form)
>>     {
>>         $auth = $this->_auth;
>>         $values = $form->exportValues();
>>
>>         $adapter = new MyAuth_Adapter();
>>         $adapter->setUsername($values['username']);
>>         $adapter->setPassword($values['password']);
>>                try {
>>             $auth->authenticate($adapter);
>>         } catch (MyAuth_Adapter_Exception_Missing $e) {
>>             // Let form know that login has failed...
>>         } catch (MyAuth_Adapter_Exception_Locked $e) {
>>             // Let form know that login has failed...
>>         }
>>                if (!$auth->isAuthenticated()) {
>>             // Let form know that password was incorrect or your  
>> account is not active...
>>         }
>>                $identity = $auth->getIdentity();
>>         // Retrieve row of user info and store inside Identity  
>> object (including role!)
>>         $userTable = new MyUser_Table; // Instance of  
>> Zend_Db_Table or similar...
>>         $identity->setUser($userTable->find($identity-
>> >getIdentifier()));
>>     }
>> }
>>
>>
>> *Conclusions:*
>> -------------
>> This is obviously an over-simplified example that attempts to  
>> address the challenges of the Acl/Auth components in relation to  
>> the MVC components.
>>
>> I believe this approach - though needing some further  
>> bulletproofing - demonstrates good practice and encourages the  
>> developer to think about logical and clean ways of separating the  
>> process of authentication. Some benefits are:-
>>
>> * You could easily drop in ACL rules in the one point and have a  
>> more complex and rich set of rules without needing any update to  
>> your controllers, relieving a lot of maintenance issues. *  
>> Processing user input happens only in a single point in an area of  
>> the application that makes it more natural for developers to  
>> understand and build upon. The filters and validation take place  
>> in a separate form model that can be replaced/updated without  
>> affecting any other portion of the process. Post-login business  
>> rules can be added without touching other plugins or the bootstrap.
>>
>> I hope this is useful! Would very much appreciate feedback - even  
>> if to say I'm doing it all wrong! :-) I'm still learning a lot  
>> from listening to the discussions on this list and I'm keen to  
>> find out how others approach this kind of layer in their own  
>> applications.
>>
>> --
>>
>> Simon Mundy | Director | PEPTOLAB
>>
>> """ " "" """""" "" "" """"""" " "" """"" " """"" "  """""" "" "
>> 202/258 Flinders Lane | Melbourne | Victoria | Australia | 3000
>> Voice +61 (0) 3 9654 4324 | Mobile 0438 046 061 | Fax +61 (0) 3  
>> 9654 4124
>> http://www.peptolab.com
>>
>>
>

--

Simon Mundy | Director | PEPTOLAB

""" " "" """""" "" "" """"""" " "" """"" " """"" "  """""" "" "
202/258 Flinders Lane | Melbourne | Victoria | Australia | 3000
Voice +61 (0) 3 9654 4324 | Mobile 0438 046 061 | Fax +61 (0) 3 9654  
4124
http://www.peptolab.com


 « Return to Thread: Zend_Acl / Zend_Auth example scenario