Practical Zend_Acl + Zend_Auth Implementation and Best Practices

Practical Zend_ACL + Zend_Auth implementation and best practices

My implementation:

Question #1

class App_Model_Acl extends Zend_Acl
{
const ROLE_GUEST = 'guest';
const ROLE_USER = 'user';
const ROLE_PUBLISHER = 'publisher';
const ROLE_EDITOR = 'editor';
const ROLE_ADMIN = 'admin';
const ROLE_GOD = 'god';

protected static $_instance;

/* Singleton pattern */
protected function __construct()
{
$this->addRole(new Zend_Acl_Role(self::ROLE_GUEST));
$this->addRole(new Zend_Acl_Role(self::ROLE_USER), self::ROLE_GUEST);
$this->addRole(new Zend_Acl_Role(self::ROLE_PUBLISHER), self::ROLE_USER);
$this->addRole(new Zend_Acl_Role(self::ROLE_EDITOR), self::ROLE_PUBLISHER);
$this->addRole(new Zend_Acl_Role(self::ROLE_ADMIN), self::ROLE_EDITOR);

//unique role for superadmin
$this->addRole(new Zend_Acl_Role(self::ROLE_GOD));

$this->allow(self::ROLE_GOD);

/* Adding new resources */
$this->add(new Zend_Acl_Resource('mvc:users'))
->add(new Zend_Acl_Resource('mvc:users.auth'), 'mvc:users')
->add(new Zend_Acl_Resource('mvc:users.list'), 'mvc:users');

$this->allow(null, 'mvc:users', array('index', 'list'));
$this->allow('guest', 'mvc:users.auth', array('index', 'login'));
$this->allow('guest', 'mvc:users.list', array('index', 'list'));
$this->deny(array('user'), 'mvc:users.auth', array('login'));

/* Adding new resources */
$moduleResource = new Zend_Acl_Resource('mvc:snippets');
$this->add($moduleResource)
->add(new Zend_Acl_Resource('mvc:snippets.crud'), $moduleResource)
->add(new Zend_Acl_Resource('mvc:snippets.list'), $moduleResource);

$this->allow(null, $moduleResource, array('index', 'list'));
$this->allow('user', 'mvc:snippets.crud', array('create', 'update', 'delete', 'read', 'list'));
$this->allow('guest', 'mvc:snippets.list', array('index', 'list'));

return $this;
}

protected static $_user;

public static function setUser(Users_Model_User $user = null)
{
if (null === $user) {
throw new InvalidArgumentException('$user is null');
}

self::$_user = $user;
}

/**
*
* @return App_Model_Acl
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new self();
}
return self::$_instance;
}

public static function resetInstance()
{
self::$_instance = null;
self::getInstance();
}
}

class Smapp extends Bootstrap // class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
/**
* @var App_Model_User
*/
protected static $_currentUser;

public function __construct($application)
{
parent::__construct($application);
}

public static function setCurrentUser(Users_Model_User $user)
{
self::$_currentUser = $user;
}

/**
* @return App_Model_User
*/
public static function getCurrentUser()
{
if (null === self::$_currentUser) {
self::setCurrentUser(Users_Service_User::getUserModel());
}
return self::$_currentUser;
}

/**
* @return App_Model_User
*/
public static function getCurrentUserId()
{
$user = self::getCurrentUser();
return $user->getId();
}

}

in class bootstrap

protected function _initUser()
{
$auth = Zend_Auth::getInstance();
if ($auth->hasIdentity()) {
if ($user = Users_Service_User::findOneByOpenId($auth->getIdentity())) {
$userLastAccess = strtotime($user->last_access);
//update the date of the last login time in 5 minutes
if ((time() - $userLastAccess) > 60*5) {
$date = new Zend_Date();
$user->last_access = $date->toString('YYYY-MM-dd HH:mm:ss');
$user->save();
}
Smapp::setCurrentUser($user);
}
}
return Smapp::getCurrentUser();
}

protected function _initAcl()
{
$acl = App_Model_Acl::getInstance();
Zend_View_Helper_Navigation_HelperAbstract::setDefaultAcl($acl);
Zend_View_Helper_Navigation_HelperAbstract::setDefaultRole(Smapp::getCurrentUser()->role);
Zend_Registry::set('Zend_Acl', $acl);
return $acl;
}

and Front_Controller_Plugin

class App_Plugin_Auth extends Zend_Controller_Plugin_Abstract
{
private $_identity;

/**
* the acl object
*
* @var zend_acl
*/
private $_acl;

/**
* the page to direct to if there is a current
* user but they do not have permission to access
* the resource
*
* @var array
*/
private $_noacl = array('module' => 'admin',
'controller' => 'error',
'action' => 'no-auth');

/**
* the page to direct to if there is not current user
*
* @var unknown_type
*/
private $_noauth = array('module' => 'users',
'controller' => 'auth',
'action' => 'login');

/**
* validate the current user's request
*
* @param zend_controller_request $request
*/
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$this->_identity = Smapp::getCurrentUser();
$this->_acl = App_Model_Acl::getInstance();

if (!empty($this->_identity)) {
$role = $this->_identity->role;
} else {
$role = null;
}

$controller = $request->controller;
$module = $request->module;
$controller = $controller;
$action = $request->action;

//go from more specific to less specific
$moduleLevel = 'mvc:'.$module;
$controllerLevel = $moduleLevel . '.' . $controller;
$privelege = $action;

if ($this->_acl->has($controllerLevel)) {
$resource = $controllerLevel;
} else {
$resource = $moduleLevel;
}

if ($module != 'default' && $controller != 'index') {
if ($this->_acl->has($resource) && !$this->_acl->isAllowed($role, $resource, $privelege)) {
if (!$this->_identity) {
$request->setModuleName($this->_noauth['module']);
$request->setControllerName($this->_noauth['controller']);
$request->setActionName($this->_noauth['action']);
//$request->setParam('authPage', 'login');
} else {
$request->setModuleName($this->_noacl['module']);
$request->setControllerName($this->_noacl['controller']);
$request->setActionName($this->_noacl['action']);
//$request->setParam('authPage', 'noauth');
}
throw new Exception('Access denied. ' . $resource . '::' . $role);
}
}
}
}

and finnaly - Auth_Controller` :)

class Users_AuthController extends Smapp_Controller_Action 
{
//sesssion
protected $_storage;

public function getStorage()
{
if (null === $this->_storage) {
$this->_storage = new Zend_Session_Namespace(__CLASS__);
}
return $this->_storage;
}

public function indexAction()
{
return $this->_forward('login');
}

public function loginAction()
{
$openId = null;
if ($this->getRequest()->isPost() and $openId = ($this->_getParam('openid_identifier', false))) {
//do nothing
} elseif (!isset($_GET['openid_mode'])) {
return;
}

//$userService = $this->loadService('User');

$userService = new Users_Service_User();

$result = $userService->authenticate($openId, $this->getResponse());

if ($result->isValid()) {
$identity = $result->getIdentity();
if (!$identity['Profile']['display_name']) {
return $this->_helper->redirector->gotoSimpleAndExit('update', 'profile');
}
$this->_redirect('/');
} else {
$this->view->errorMessages = $result->getMessages();
}
}

public function logoutAction()
{
$auth = Zend_Auth::getInstance();
$auth->clearIdentity();
//Zend_Session::destroy();
$this->_redirect('/');
}
}

Question #2

keep it inside Zend_Auth.

after succesfull auth - write identity in storage. $auth->getStorage()->write($result->getIdentity());

the identity - is simply user_id

DB design

CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`open_id` varchar(255) NOT NULL,
`role` varchar(20) NOT NULL,
`last_access` datetime NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `open_id` (`open_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `user_profile` (
`user_id` bigint(20) NOT NULL,
`display_name` varchar(100) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`real_name` varchar(100) DEFAULT NULL,
`website_url` varchar(255) DEFAULT NULL,
`location` varchar(100) DEFAULT NULL,
`birthday` date DEFAULT NULL,
`about_me` text,
`view_count` int(11) NOT NULL DEFAULT '0',
`updated_at` datetime NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

some sugar

/**
* SM's code library
*
* @category
* @package
* @subpackage
* @copyright Copyright (c) 2009 Pavel V Egorov
* @author Pavel V Egorov
* @link http://epavel.ru/
* @since 08.09.2009
*/

class Smapp_View_Helper_IsAllowed extends Zend_View_Helper_Abstract
{
protected $_acl;
protected $_user;

public function isAllowed($resource = null, $privelege = null)
{
return (bool) $this->getAcl()->isAllowed($this->getUser(), $resource, $privelege);
}

/**
* @return App_Model_Acl
*/
public function getAcl()
{
if (null === $this->_acl) {
$this->setAcl(App_Model_Acl::getInstance());
}
return $this->_acl;
}

/**
* @return App_View_Helper_IsAllowed
*/
public function setAcl(Zend_Acl $acl)
{
$this->_acl = $acl;
return $this;
}

/**
* @return Users_Model_User
*/
public function getUser()
{
if (null === $this->_user) {
$this->setUser(Smapp::getCurrentUser());
}
return $this->_user;
}

/**
* @return App_View_Helper_IsAllowed
*/
public function setUser(Users_Model_User $user)
{
$this->_user = $user;
return $this;
}

}

for things like this in any view script

 <?php if ($this->isAllowed('mvc:snippets.crud', 'update')) : ?>
<a title="Edit «<?=$this->escape($snippetInfo['title'])?>» snippet">Edit</a>
<?php endif?>

Questions? :)

Zend_Auth best practices

  • I want non-logged in users to get a login box, and then return to logged
    in version of the page, once
    authenticated

Use a FrontController plugin and redirect or forward them to your loginAction.

  • I want to use dependency injection, and avoid singletons

Zend Framework, doesn't currently ship any DI system, however, the Zend_Application_Resource_* actually replace it. What kind of dependency would you need here?

  • Small code footprint - tie into Zend mvc structure

That's up to you.

  • Should login box be a separate controller and do header redirect? How
    to return to landing page after auth
    success? An idea to simply call the
    login controller action to display the
    login box in the landing page, or is
    this a disadvantage regarding search
    engine indexing?

I mostly use a special AuthController with LoginAction & LogoutAction. To redirect the user to the page is was trying to view, I always add a returnUrl element in my forms, and I inject the value of the requested URL to be able to redirect the user, and if none, I redirect him to the index/dashboard, depends.

  • Be able to use external library for handling cookies

Zend_Auth allows you to set your own storage mechanism, so just implement the interface.

$auth = Zend_Auth::getInstance();
$auth->setStorage(new My_Auth_Storage());

But never store authentication result in a cookie, it's so easy to modify it and access your website.

You may also take a look to one of my previous answer.

Zend_Auth or Zend_Acl in checking against DB record?

I think your approach makes sense. I would store the book ID in a Zend_Session_Namespace and store the user in the storage of Zend_Auth. If the user you are putting in the storage is an object/model, have it implement Zend_Acl_Role_Interface so it contains a getRoleId() method that returns the user's role. This way you can pass your user object directly to any ACL method that requires a role and it can get the role from the object.

If it makes sense, you could have a property of your user object called activeBookId or something like that and store the book ID there if it seems like it can fit into the user object.

Difference between Zend_auth & Zend_acl?

Zend Auth is used for authenticate the end user , it handles the login and session managment while zend acl is a diffrent thing . You can deside the role of the loged in user like admin , manager , customer atc. I preffer to see zend documentation for more information.

Where can I find the best way of implementing Zend_Auth?

I use the following method to authenticate:

function authenticate ($data)
{
$db = \Zend_Db_Table::getDefaultAdapter();
$authAdapter = new \Zend_Auth_Adapter_DbTable($db);

$authAdapter->setTableName('usuarios2');
$authAdapter->setIdentityColumn('user');
$authAdapter->setCredentialColumn('password');
$authAdapter->setCredentialTreatment('MD5(?) and active = 1');

$authAdapter->setIdentity($data['user']);
$authAdapter->setCredential($data['password']);

$auth = \Zend_Auth::getInstance();
$result = $auth->authenticate($authAdapter);

if ($result->isValid()) {

if ($data['public'] == "1") {
\Zend_Session::rememberMe(1209600);
} else {
\Zend_Session::forgetMe();
}

return TRUE;

} else {

return FALSE;

}
}

$data is the post request from the login form, from the controller I call the function like this:
authenticate($this->_request->getPost())

In any action if you want to verify the identity of the user you just:

$auth = Zend_Auth::getInstance();
if ($auth->hasIdentity()) {
$identity = $auth->getIdentity(); //this is the user in my case
}

At the login form I have a checkbox (named public) if its checked the the authentication information will be saved in a cookie otherwhise it will be deleted when the user closes the browser (Zend_Session::forgetMe())

This is a quick review of the auth process.

Zend_Acl, Zend_Auth and Modules

You have constructed the ACL object. You need to use it now, every time you need to check whether the current user has access to one of the resources.

To do so, you need to retrieve your ACL object from the registry:

$acl = Zend_Registry::get('acl');

And then query it:

if ($acl->isAllowed($yourUser, 'members')) {
// Do the job
} else {
// some message or redirection
}

You can see more details in the Zend_Acl documentation.

Hope that helps,

Zend_Acl and Zend_Auth api key approach

Zend_Auth will handle most of the authentication for you. Use something along

$auth = Zend_Auth::getInstance();
if (!$auth->hasIdentity()) {
//call a custom login action helper to try login with GET-params
}
if ($auth->hasIdentity())
$identity = $auth->getIdentity();
//...
}

Now you can determine the Zend_Acl_Role based on the identity. I always create a new role for each user and let this role 'inherit' all generic roles that the user actually has.

// specific user with $identity is given the generic roles staff and marketing
$acl->addRole(new Zend_Acl_Role('user'.$identity), array('staff', 'marketing'));

Of course you can retrieve the array of roles from a database. Then you have to specify the rights of each role. You can hard code that or save these information in a database as well.

$acl->allow('marketing',
array('newsletter', 'latest'),
array('publish', 'archive'));

In your controller you can now check

$acl->isAllowed('user'.$identity, Zend_Acl_Resource ...)

If you have a more complex access control where the rights depend on the information inside some classes (probably MCV models), have these classes implement the Zend_Acl_Resource_Interface. Then you use this class as a parameter of a Zend_Acl_Assertion and handle the information there.

Zend_ACL - Cannot use my current model with it, I think I'm doing MVC the wrong way here

I'm afraid I'm not answering your question, but here are some really good articles to read. Maybe you'll find help there :

  • http://codeutopia.net/blog/2009/02/06/zend_acl-part-1-misconceptions-and-simple-acls/
  • Maybe you can read french ? -> http://www.kitpages.fr/fr/cms/89/initiation-a-zend_acl

Multiple Instances (2) of Zend_Auth

In that case, you want to create your own 'Auth' class to extend and remove the 'singleton' design pattern that exists in Zend_Auth

This is by no means complete, but you can create an instance and pass it a 'namespace'. The rest of Zend_Auth's public methods should be fine for you.

<?php
class My_Auth extends Zend_Auth
{

public function __construct($namespace) {
$this->setStorage(new Zend_Auth_Storage_Session($namespace));
// do other stuff
}
static function getInstance() {
throw new Zend_Auth_Exception('I do not support getInstance');
}
}

Then where you want to use it, $auth = new My_Auth('CMSUser'); or $auth = new My_Auth('SiteUser');

Zend_Auth replacement?

First (just to be clear): everything under library is called a component and not a module. Also, ZF2 is extremely beta so expect things will change often and no backwards compatibility is guaranteed.

Then, the Zend_Auth component has been renamed to Zend\Authentication. The first change is the underscore separation to namespaces, the second change is the renaming of Auth to Authentication. As far as I know, the features from Zend_Auth are all ported over to Zend\Authentication, the new component is also on GitHub.



Related Topics



Leave a reply



Submit