Zend_Acl / Zend_Auth example scenario

11 messages Options
Embed this post
Permalink
Simon Mundy

Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
Some javascript/style in this post has been disabled (why?)
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:
-------------------

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'];
Bryce Lohr

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
I this a lot; there are a lot of good ideas in there. It seems to me like it
plays nice with the MVC framework. Personally, I think it would be a good start
for Gavin's tutorial. :)

I think for doing forms-based authentication, this is exactly what you'd want to
do. At the moment, I'm still parsing through this trying to figure how to apply
it to HTTP Authentication. If I figure it out, I might update the HTTP
Authentication Auth adapter to better fit this set up.

Regards,
Bryce Lohr


Simon Mundy wrote:
> 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.
nervo-3

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
In reply to this post by Simon Mundy
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

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
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


Wojciech Naruniec

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
In reply to this post by nervo-3
Some javascript/style in this post has been disabled (why?)
Hi,

You can use something like this:

$auth = new Zend_Auth(new My_Auth_Db_Adapter());


Greetings,
Wojciech Naruniec



On 07.02.2007, at 13:15 PM, nerVo wrote:

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

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()) {
dropdownInit('dropdown676312'); if (Nabble.searchterms != null && Nabble.searchterms.length > 0) { Nabble.hilt(Nabble.searchterms, Nabble.get("nabble.msgtxt676312")); Nabble.hilt(Nabble.searchterms, Nabble.get("post-subject676312")); }
Jim Scherer

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
In reply to this post by Simon Mundy
Simon,

Great example but I have two questions. I'm getting a 'Fatal error: Can not call constructor' any thoughts? What is the point of $roleGuest? I'm able to just do this

         $this->addRole(new Zend_Acl_Role('guest'));
         $this->addRole(new Zend_Acl_Role('member'), 'guest');
         $this->addRole(new Zend_Acl_Role('admin'), 'member');

and everything seems fine. Why do you break out $roleGuest?

The framework is very valuable to me. It is helping me to organize and simplify my code while reducing the amount of code I am writing. The examples are educating about how to best utilize the framework and php in general. Keep up the good work and please continue post usage examples.

Thanks, Jim

Simon Mundy wrote:
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
     }
}
Simon Mundy

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
Hi Jim

Nothing more than being slightly careless when copy and pasting an  
example from Darby's documentation :)

Storing $roleGuest as a new Zend_Acl_Role was only originally there  
to use when setting inheritance when defining new roles, but since I  
referred to them as strings only... well... yer right! $roleGuest is  
a bit redundant in that situation.

I was also keen in the example to explore how the interaction between  
user -> model -> auth worked in this example and if people were of  
the opinion that this was a more 'correct' strategy.

> Great example but I have two questions. I'm getting a 'Fatal error:  
> Can not
> call constructor' any thoughts? What is the point of $roleGuest?  
> I'm able to
> just do this
>
>          $this->addRole(new Zend_Acl_Role('guest'));
>          $this->addRole(new Zend_Acl_Role('member'), 'guest');
>          $this->addRole(new Zend_Acl_Role('admin'), 'member');
>
> and everything seems fine. Why do you break out $roleGuest?
>
> The framework is very valuable to me. It is helping me to organize and
> simplify my code while reducing the amount of code I am writing. The
> examples are educating about how to best utilize the framework and  
> php in
> general. Keep up the good work and please continue post usage  
> examples.

--

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


Ralf Eggert

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
In reply to this post by Simon Mundy
Hi Simon,

thank you very much for this thought-out example of how to use Zend_Acl
and Zend_Auth together. Since I am in the middle of adding these to
components to my project I found a lot of similarities with my approach
so far but also some good suggestions how to handle things in a
different way.

When I am finished with my solutions I might post it to this list as
well so we can select a few examples on how to handle these components.

Best Regards,

Ralf

FreeQ

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
Hi,
Can you make package with code, which is make all files. I'm trying to understend Zend Auth and Zend Acl. I just read code from previous messages, but I can't compact all together.
Please, help me understand it.

Bye
Jarek

ps. I'm sorry for my English. I'm still learning it.
paperogiallo

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
In reply to this post by Simon Mundy
Hi Simon,
I really appreciate your ideas, and with some efforts due to ZF-code modification in the mean time, I've understood quite all you posted. So, thanks a lot. :)

But...

1) the plugin seems to "intercept" all requests, even those to non-existent controllers or actions: is it a matter of hook in the request&dispatch loop? Is there any patch to this (supposed) bug?
2) please, could you provide an example of the "domain model - MyForm_Login"? Or maybe, point me to some useful resources about this? I don't understand if "domain model" is some kind of "controller+model" (but where is it in the application), or whatelse?

Thanks again (and sorry my poor English!)

paperogiallo

Simon Mundy wrote:
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
()));
     }
}
Mark Maynereid

Re: Zend_Acl / Zend_Auth example scenario

Reply Threaded More More options
Print post
Permalink
Hi,

I've have also found this problem that the auth plugin breaks the error handler plugin on 404 Page not found requests.  Make sense to me. The auth plugin is run in preDispatch while the built-in error handler plugin doesn't get a look in until postDispatch.

A perhaps undesirable fix that works for me is to replace:
if (!$this->_acl->has($resource)) {
    $resource = null;
}
with:
if (! $this->_acl->has($resource)) {
    return;
}
in the auth plugin.

This means the plugin enforces only on resources it's been told about. So allowing the error handler access to requests that turn out to be 404 page not found. But if you forget to register a controller as a an acl resource the whitelist policy is lost for that controller. Not ideal maybe.

Has anyone got a better fix?

Regards,
Mark

ps.
I get behaviour regardless of $front->throwExceptions() settings.

paperogiallo wrote:
Hi Simon,
I really appreciate your ideas, and with some efforts due to ZF-code modification in the mean time, I've understood quite all you posted. So, thanks a lot. :)

But...

1) the plugin seems to "intercept" all requests, even those to non-existent controllers or actions: is it a matter of hook in the request&dispatch loop? Is there any patch to this (supposed) bug?