开发者

Practical Zend_ACL + Zend_Auth implementation and best practices

Context:

My questions pertain to a forum I'm developing pretty much exactly like SO, where there are:

  1. guests who have access to view threads but can't reply or vote
  2. members who, with enough rep, can edit/vote others threads, and by default they can reply and have the same privileges as guests
  3. admins who can pretty much do anything

I would want this ACL to be applied site-wide, and by default deny all resources.

I read the basics of using Zend_Acl - in that you basically create roles ( guest, member, admin ) and either deny or allow resources ( controllers, methods ) to those roles. The documentation isn't very specific on how you should actually implement the acl code in your application, so I went looking on SO..

Came across a pretty useful stackoverflow answer from marek which sheds some light on the issue, however due to my unfamiliarity I still can't fully grok how to properly implement this with best practices in mind.

The poster has a static file configAcl.php in the application root which initializes the acl object, adds roles, creates a resource out of every controller, gives admin access to everything, gives normal access to everything but the admin and stores the acl object in the registry for later use.

$acl = new Zend_Acl();

$roles  = array('admin', 'normal');

// Controller script names. You have to add all of them if credential check
// is global to your application.
$controllers = array('auth', 'index', 'news', 'admin');

foreach ($roles as $role) {
    $acl->addRole(new Zend_Acl_Role($role));
}
foreach ($controllers as $controller) {
    $acl->add(new Zend_Acl_Resource($controller));
}

// Here comes credential definiton for admin user.
$acl->allow('admin'); // Has access to everything.

// Here comes credential definition for normal user.
$acl->allow('normal'); // Has access to everything...
$acl->deny('normal', 'admin'); // ... except the admin controller.

// Finally I store whole ACL definition to registry for use
// in AuthPlugin plugin.
$registry = Zend_Registry::getInstance();
$registry->set('acl', $acl);

Question #1 - Should this code be in the bootstrap, or in a standalone file such as this? If so would it be better if it was inside say, the library directory?

The second part of it is a new class extending the Zend Controller Plugin Abstract class which allows it to be hooked into auth/login, the logic is basically if the login fails, it redirects.. otherwise it grabs the acl object from the registry, grabs the identity, 开发者_开发百科and determines if the user is allowed to view this resource.

$identity = $auth->getIdentity();

$frontController->registerPlugin(new AuthPlugin());

Question #2 - How exactly would I code the auth plugin part that actually returns the identity of the user? I realize that he had some code below that generated a Auth adapter db table object which would query a database table's column by user id and credential ( hashed pass check ).. I'm confused on where this fits in with the getIdentity part.

Let's say my users table was composed of this data:

user_id    user_name    level
1          superadmin   3
2          john         2
3          example.com  1

Where level 3 = admin, 2 = member, 1 = guest.

Question #3 - where exactly is a good place to put the above auth code in? Inside of the login controller?

Question #4 - another poster replies with his article on how the acl logic should be done inside models, yet the specific method which he uses is not natively supported and requires a workaround, is this feasible? And is this really how it ideally should be done?


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 &laquo;<?=$this->escape($snippetInfo['title'])?>&raquo; snippet">Edit</a>
 <?php endif?>

Questions? :)

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜