Scalable Authorization with Zend Framework

The Zend Framework offers a highly evolved and complex control flow model. The ZF provides three distinct ways to insert code – Front Plugins, Action Helpers and Controllers. The interplay between these is best expressed by the  diagram at http://surlandia.com/wp-content/uploads/2008/11/zf-dispatch-lifecycle-bw.jpg

Authorization to a page(controller/action) is to be checked on EVERY page request.

The main mechanism of implementing Authorization in the ZF is using Zend_ACL. If you are not careful with implementing this, you could easily end up loading the ACL object with EVERY action and controller in the application. We do NOT want to spend the time and memory loading the ACL with resources that will never be required!

A couple of months ago, I had written about how the ZF authorization could be automated using database tables. While this approach tries to alleviate some of the problems associated with dynamic ACL loading using a session variable, it is still wasteful of resources and is not advisable for large apps.

The following technique is, in my opinion, a smarter approach to handling ACL’s and authorization (Credit goes to Rob Allen, author of Zend Framework in Action). The key to using this method is to understand the correct place to implement ACL checking and rule creation.

image

Here is a simplified flowchart of what we plan to do:

image

Create a custom ACL class and load all the roles (along with inheritance information). For establishing the inheritance hierarchy, we can add the following lines to the config.ini

acl.roles.guest=null
acl.roles.member=guest
acl.roles.admin=member

the member role inherits from guest and admin role inherits from member.. Simple!

<?php
class CustomACL extends Zend_Acl
{
public function __construct()
{
$config = Zend_Registry::get(‘config’);
$roles = $config->acl->roles;
$this->addRoles($roles);
}
protected function addRoles($roles)
{
foreach ($roles as $child=>$parents)
{
if (!$this->hasRole($child))
{
if (empty($parents))
$parents=null;
else
$parents = explode(‘,’,$parents);
$this->addRole(new Zend_Acl_Role($child),$parents);
}
}
}
}
?>

Next, we code the Action Controller Plugin:

<?php
class AuthHelper extends Zend_Controller_Action_Helper_Abstract
{
protected $auth;
protected $acl;
protected $controllerName;
protected $actionName;
protected $defaultRole=’guest’;
public function __construct(Zend_View_Interface $view=null, array $options)
{
$this->auth=Zend_Auth::getInstance();
$this->acl = $options[‘acl’];
}
public function preDispatch()
{
$this->controllerName = $this->getActionController()->getRequest()->getControllerName();
$this->actionName = $this->getActionController()->getRequest()->getActionName();
if (!$this->acl->has($this->controllerName))
{
$this->acl->add(new Zend_Acl_Resource($this->controllerName));
}
if ($this->auth->hasIdentity())
{
$session_role = new Zend_Session_Namespace(‘role’);
$role=$session_role->role;
}
else
$role=$this->defaultRole;
//redirect to login page if not authorized!
if (!$this->acl->isAllowed($role,$this->controllerName, $this->actionName))
{
$this->getActionController()->getRequest()->setControllerName(‘auth’);
$this->getActionController()->getRequest()->setActionName(‘login’);
$this->getActionController()->getRequest()->isDispatched()=false; //this line is important!

}
}
public function allow($roles=null, $actions=null)
{
$resource = $this->controllerName;
$this->acl->allow($roles,$resource, $actions);
return $this;
}
public function deny($roles=null, $actions=null)
{
$resource = $this->controllerName;
$this->acl->deny($roles,$resource, $actions);
return $this;
}
}
?>

PLEASE NOTE the $this->getActionController()->getRequest()->isDispatched()=false directive immediately after setting the new controller and action. If this is not included, it would execute the code within the original action anyway .. although the results will be discarded from buffer and never rendered on screen (You can put in a log message to verify this!). Adding this directive will skip the code in the action function. This will make more sense after you understand the detailed dispatch control flow within the Zend Framework.

dispatch

The index.php bootstrapper is modified accordingly to load up the front controller plugin above:

$acl = new CustomACL();
$authHelper = new AuthHelper(null,array(‘acl’=>$acl));
Zend_Controller_Action_HelperBroker::addHelper($authHelper);

Here’s what my BaseController.php and AuthController.php look like:

<?php
class BaseController extends Zend_Controller_Action
{
protected $db;
protected $auth;
public function preDispatch()
{
$this->db = Zend_Db_Table::getDefaultAdapter();
$this->view->baseUrl = Zend_Controller_Front::getInstance()->getBaseUrl();
$this->auth=Zend_Auth::getInstance();
}
}
?>

<?php
class AuthController extends BaseController
{
public function init()
{
$this->_helper->AuthHelper->allow(null);
}
public function indexAction()
{
$this->_forward(‘login’);
}
public function loginAction()
{
if ($this->auth->hasIdentity())
$this->_redirect(‘index’);

$config = Zend_Registry::get(‘config’);
$errors = array ();
$request = $this->getRequest();

if ($request->isPost())
{
$redirect = $request->getPost(‘redirect’);
$username = $request->getPost(‘username’);
$password = $request->getPost(‘password’);
if (strlen($username) == 0)
$errors[‘username’] = ‘Username is a required
field’;
if (strlen($password) == 0)
$errors[‘password’] = ‘password is a required field’;
if (count($errors) == 0)
{
//data is valid.. do authenticate
$authAdapter = $this->getAuthAdapter($username, $password);
$result = $this->auth->authenticate($authAdapter);

if ($result->isValid())
{
//success.. store data in session..
//and redirect
$this->getRole($username);
$this->_redirect($redirect);

}
else
{
//ldap auth failed
$errors[‘auth’] = “LDAP authentication failed.. please check username and password”;
}
}
}

$this->view->errors = $errors;
$this->view->redirect = $redirect;
//throw the login page again!
}
public function logoutAction()
{
$this->auth->clearIdentity();
$this->_redirect(‘/’);
}
protected function getAuthAdapter($username, $password)
{
$config = Zend_Registry::get(‘config’);
//get the ldap adapter and return to caller.
$authAdapter = new Zend_Auth_Adapter_Ldap($config->ldap->toArray(), $username, $password);
//print_r($username.$password);
return $authAdapter;
}
protected function getRole($username)
{
$db = Zend_Db_Table::getDefaultAdapter();
$result = $db->fetchOne(“select role from users where userid=?”,$username);
$session_role = new Zend_Session_Namespace(‘role’);
$session_role->role=$result;
}
}
?>

The loginAction() attempts an LDAP authentication. If successfully authenticated, a call is made to getRole(). This function looks up the database table named ‘users’ and determines the ‘role’ to which the user belongs. This role is stored in a session variable – Note that this is accessed in the preDispatch() method of our action controller plugin.

Also note the init() function, it basically gives everyone access to the Auth controller.

The login.phtml file code follows:

<?php
$errors = $this->errors;
if (strlen($this->redirect)==0)
$redirect = str_replace(“/internal/inventory/”, “/”, $_SERVER[‘REQUEST_URI’]);
else
$redirect = $this->redirect;
?>
<h2>Sorry! You are not authorized to use the section</h2>
<h1>Please login here</h1>

<div class=”error”> <?php if (isset($errors[‘auth’])) echo $errors[‘auth’] ?></div>
<form method=”post” action=”<?php echo $this->baseUrl?>/auth/login”>
<input type=”hidden” name=”redirect” value=”<?php echo  $redirect?>” />
<div>
<label>
Username
</label>
<input type=”text” name=”username” value=””/>
<div class=”error”><?php if (isset($errors[‘username’])) echo $errors[‘username’]?></div>
</div>
<div>
<label>
Password
</label>
<input type=”password” name=”password” value=”” />
<div class=”error”><?php if (isset($errors[‘password’])) echo $errors[‘password’]?></div>
</div>
<div>
<input type=”submit” name=”login” value=”Login”/>
</div>
</form>

Note the use of the ‘Redirect’ hidden variable… That is used to redirect users to the original page after due authentication.

Thats it! Now, you just have to add init() functions on your controllers to specify ACL rules.. The rest is handled at runtime!

A more involved init() function would look like:

class InternalController extends BaseController {

function init()
{
$guestActions = array(‘index’);
$this->_helper->AuthHelper->allow(‘guest’, $guestActions);
$adminActions = array(‘add’, ‘edit’);
$this->_helper->AuthHelper->allow(‘admin’, $adminActions);
}

……

……

}

What the above code does is grant the ‘guest’ role access to /internal/index and demand the ‘admin’ role for urls ‘/internal/add’ and ‘/internal/edit’.

Thats it! Please feel free to drop your comments and questions.