« Return to Thread: Zend_Acl / Zend_Auth example scenario

Re: Zend_Acl / Zend_Auth example scenario

by nervo-3 :: Rate this Message:

Reply to Author | View in Thread

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
>
>

 « Return to Thread: Zend_Acl / Zend_Auth example scenario