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