Authorization with Zend_Acl in Zend Framework 1.8

Exclusive offer: get 50% off this eBook here
Zend Framework 1.8 Web Application Development

Zend Framework 1.8 Web Application Development — Save 50%

Design, develop, and deploy feature-rich PHP web applications with this MVC framework

$23.99    $12.00
by | October 2009 | MySQL Open Source PHP Web Development

In this article by Keith Pope, we will focus on how we can control access to parts of the application and how users can log in to use the services provided by the Storefront.

We will cover the following topics:

  • Using Zend_Acl
  • Integrating the ACL into our Models

Read Authentication with Zend_Auth in Zend Framework 1.8 to learn authentication in Zend Framework.

Authorization with Zend_Acl in Zend Framework

We now have a way to check if a user is who he/she says he/she is. Next we need to stop certain users from accessing certain parts of the application. To do this, we are going to use the Zend_Acl component

Zend_Acl introduction

ACL (Access Control List) lets us create a list that contains all the rules for accessing our system. In Zend_Acl, this list works like a tree enabling us to inherit from rule to rule, building up a fi ne-grained access control system.

There are two main concepts at work in Zend_Acl—Resources and Roles. A Resource is something that needs to be accessed and a Role is the thing that is trying to access the Resource. To have access to a resource, you need to have the correct Role.

To start us off, let's fi rst look at a basic example. In this example, we are going to use the scenario of a data centre. In the data centre, we need to control access to the server room. Only people with the correct permissions will be able to access the server room.

To start, we need to create some Roles and Resources.

$visitor = new Zend_Acl_Role('Visitor');
$admin = new Zend_Acl_Role('Admin');
$serverRoom = new Zend_Acl_Resource('ServerRoom');

Here we have created two Roles—Visitor and Admin and one Resource—ServerRoom. Next, we need to create the Access Control List.

$acl = new Zend_Acl();
$acl->addRole($visitor);
$acl->addRole($admin, $visitor);
$acl->add($serverRoom);

Here we instantiate a new Zend_Acl instance and add the two Roles and one new access rule. When we add the Roles, we make the Admin Role inherit from the Visitor Role. This means that Admin inherits all the access rules of the Visitor. We also add one new Rule containing the ServerRoom resource.

At this point, access to the server room is denied for both Visitors and Admins. We can change this by adding allow or deny rules:

  • Allow all to all resources: $acl->allow();
  • Deny all to all resources: $acl->deny();
  • Allow Admin and Deny Visitor to all resources: $acl->allow($admin);
  • Allow Admin and Deny Visitor to ServerRoom resource: $acl->allow($admin, $serverRoom);

When adding rules, we can also set permissions. These can be used to deny/allow access to parts of a Resource. For example, we may allow visitors to view the server room but not access the cabinets. To do this, we can add extra permission options to our rules.

Allow Visitor and Admin to view the ServerRoom, Deny Visitor cabinet access:

$acl->allow(visitor, $serverRoom, array('view'));
$acl->deny($visitor, $serverRoom, array('cabinet'));

Here we simply add the new permissions as an array containing the strings of the permissions we want to add to the ServerRoom resource.

Next we need to query the ACL. This is done through the isAllowed() method.

$acl->isAllowed($admin, $serverRoom, 'view');
// returns true
$acl->isAllowed($visitor, $serverRoom, 'view');
// returns true
$acl->isAllowed($visitor, $serverRoom, 'cabinet');
// returns false

As we can see, Zend_Acl provides us with an easy, lightweight way of controlling access to our systems resources. Next we will look at the ways in which we can use the ACL component in our MVC application.

ACL in MVC

When looking to implement ACL in MVC, we need to first think about how and where we implement the ACL in the MVC layers. The ACL by nature is centralized, meaning that all rules, permissions, and so on are kept in a central place from which we query them. However, do we really want this? What about when we introduce more than one module, do all modules use the same ACL? Also we need to think about where access control happens—is it in the Controller layer or the Model/Domain layer?

Using a centralized global ACL

A common way to implement the ACL is to use a centralized ACL with access controlled at the application level or outside the domain layer. To do this, we first create a centralized ACL. Typically, this would be done during the bootstrap process and the full ACL would be created including all rules, resources, and roles. This can then be placed within the Registry or passed as an invoke argument to the Front Controller. We would then intercept each request using a Front Controller plugin (preDispatch). This would check whether the request was authorized or not using the ACL. If the request was not valid, we would then redirect the request to an access denied controller/action.

This approach would base its rules on the controller/action being requested, so a rule using this may look something like:

$acl->allow('Customer', 'user', 'edit');

Here we would allow access for a Customer Role to the User Resource and the Edit permission. This would map to the user Controller, and the edit action or user/edit

The advantages of using centralized global ACL are as follows:

  • Centralized place to access and manage ACL rules, resources, and roles
  • Maps nicely to the MVC controller/action architecture

The disadvantages are as follows:

  • Centralized ACL could become large and hard to manage
  • No means to handle modules
  • We would need to re-implement access controls in order to use our Domain in a web service, as they are based on action/controller

Using module specific ACL's

The next logical step is to split the ACL so that we have one ACL per module. To do this, we would still create our ACL during bootstrap but this time we would create a separate ACL for each module, and then we would use an Action Helper instead of Front Controller plugin to intercept the request (preDispatch).

Advantages:

  • Fixes our module handling problem with the previous approach
  • Keeps things modular and smaller

Disadvantages:

  • We still have the problem of having to re-implement access control if we use our Domain away from the controller/action context.

ACL in the Domain layer

To deal with our last concern about what if we need to use the Domain in another context outside the controller/action architecture, we have the option to move all the Access Control into the Domain itself. To do this, we would have one ACL per module but would push the management of this into the Model. The Models would then be responsible for handling their own access rules. This in effect will give us a e-centralized ACL, as the Models will add all rules to the ACL.

Advantages:

  • We can use the Model in different contexts without the need to re-implement the access control rules.
  • We can unit test the access control
  • The rules will be based on Model methods and not depend on the application layer

Disadvantages:

  • Adds complexity to the Domain/Models
  • Being de-centralized, it could be harder to manage

For the Storefront, we have opted to use the Model based ACL approach. While it adds more complexity and implementation can be a little confusing, the advantages of being able to unit test and use the Models outside the application layer is a big advantage. It also gives us a chance to demonstrate some of the more advanced features of the ACL component.

Zend Framework 1.8 Web Application Development Design, develop, and deploy feature-rich PHP web applications with this MVC framework
Published: September 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Model based ACL

The first thing to look at is some of the main components that we need to implement a Model based ACL. The main elements here are as follows:

  • The ACL: This stores the roles we want in the system and any global rules.
  • Resource(s): This will be our Model. The Model will become the Resource we wish to access.
  • Roles: These are the actual roles we wish to use.

As we can see there is nothing new here, we are just going to be implementing them in a different way than in our earlier examples. To do this, we are going to need to create some new classes and refactor some old ones. We will start though by looking at the main ACL class and Roles.

The Storefront ACL

We will need a Zend_Acl instance for the storefront module. We will use this to store all our roles and rules.

application/modules/storefront/models/Acl/Storefront.php
class Storefront_Model_Acl_Storefront extends Zend_Acl implements SF_Acl_Interface
{
public function __construct()
{
$this->addRole(new Storefront_Model_Acl_Role_Guest)
->addRole(new Storefront_Model_Acl_Role_Customer, 'Guest')
->addRole(new Storefront_Model_Acl_Role_Admin, 'Customer');
$this->deny();
}
}

The Storefront_Model_Acl_Storefront class will store all the rules that the storefront module's Models add to it. This class subclasses the Zend_Acl class and using the constructor adds the available roles to the ACL tree. We add three roles to the ACL—Guest, Customer, and Admin. The Customer role inherits from the Guest and the Admin role inherits from the Customer. We then deny access to all resources using the deny() method with no parameters. This effectively creates a white-list, meaning that everything is denied unless we explicitly allow it.

We can see that the Storefront_Model_Acl_Storefront class is very simple and is only responsible for setting up the roles. The Models will defi ne the resources and permissions later.

The Storefront roles

Previously, we added the three storefront user roles to the ACL. We will need to create the Role classes for each of them. To create a Role class, we need to implement the Zend_Acl_Role_Interface interface. This interface defines one method (getRoleId()), which should return the role identity string.

application/modules/storefront/models/Acl/Role/Admin.php
class Storefront_Model_Acl_Role_Admin implements Zend_Acl_Role_Interface
{
public function getRoleId()
{
return 'Admin';
}
}

application/modules/storefront/models/Acl/Role/Customer.php
class Storefront_Model_Acl_Role_Customer implements Zend_Acl_Role_Interface
{
public function getRoleId()
{
return 'Customer';
}
}


application/modules/storefront/models/Acl/Role/Guest.php
class Storefront_Model_Acl_Role_Guest implements Zend_Acl_Role_Interface
{
public function getRoleId()
{
return 'Guest';
}
}

Here we have created the three roles available to the storefront (Admin, Customer, and Guest). Each one simply implements the Zend_Acl_Role_Interface and returns a string that uniquely identifi es it. However, there is one more class that needs to implement the Zend_Acl_Role_Interface, which is the Storefront_Resource_User_Item class.

application/modules/storefront/models/resources/User/Item.php
class Storefront_Resource_User_Item
extends SF_Model_Resource_Db_Table_Row_Abstract implements
Storefront_Resource_User_Item_Interface, Zend_Acl_Role_Interface
{
/*... */
public function getRoleId()
{
if (null === $this->getRow()->role) {
return 'Guest';
}
return $this->getRow()->role;
}
}

Here we have updated our user model resource item to implement the Zend_Acl_Role_Interface and added the getRoleId() method. This method either returns the user's current role column or Guest if the role is not set. We do this because we are storing the user item in the Zend_Auth identity and will be passing this to the Models for them to use for authorization checks. We can then be sure that the Models are using a valid identity.

The Storefront resources

As we said before in this implementation, the Model will be the Resource. The way we achieve this is very simple. All we need to do is implement the Zend_Acl_Resource_Interface, and this will then turn our Models (or any class) into valid ACL Resources. Here is a basic example:


class MyClass implements Zend_Acl_Resource_Interface
{
public function getResourceId()
{
return 'MyResource';
}
}

Once our class has implemented the resource interface, we can use it with the ACL:

$resource = new MyClass();
$acl = new Zend_Acl();
$acl->add($resource);

We are half way to having the ACL integrated into our Models. Next, we will create some abstract classes and interfaces that our Models can use to fully implement all the required functionality.

The new base model

To fully use the ACL, our Models need access to the ACL, the resource, and the identity. In our implementation these are Storefront_Model_Acl_Storefront, the Model ($this), and Storefront_Resource_User_Item (stored in Zend_Auth). To make these available to the Model, we are going to need some extra functionality added to each of our Models. To encapsulate this, we are going to create a new abstract model class and some new interfaces.

library/SF/Model/Acl/Interface.php
interface SF_Model_Acl_Interface
{
public function setIdentity($identity);
public function getIdentity();
public function setAcl(SF_Acl_Interface $acl);
public function getAcl();
public function checkAcl($action);
}

Here we have defi ned a new interface for our Models. This also introduces the SF_Model_Acl namespace. As not all of our Models will use the ACL, we will make it optional whether the Model uses the SF_Model or SF_Modle_Acl classes. The interface defi nes fi ve methods. These will be used to set and get the identity and ACL and also to query the ACL.

library/SF/Model/Acl/Abstract.php

abstract class SF_Model_Acl_Abstract extends SF_Model_Abstract
implements SF_Model_Acl_Interface, Zend_Acl_Resource_Interface
{
protected $_acl;
protected $_identity;
public function setIdentity($identity)
{
if (is_array($identity)) {
if (!isset($identity['role'])) {
$identity['role'] = 'Guest';
}
$identity = new Zend_Acl_Role($identity['role']);
} elseif (is_scalar($identity) && !is_bool($identity)) {
$identity = new Zend_Acl_Role($identity);
} elseif (null === $identity) {
$identity = new Zend_Acl_Role('Guest');
} elseif (!$identity instanceof Zend_Acl_Role_Interface) {
throw new SF_Model_Exception('Invalid identity provided');
}
$this->_identity = $identity;
return $this;
}
public function getIdentity()
{
if (null === $this->_identity) {
$auth = Zend_Auth::getInstance();
if (!$auth->hasIdentity()) {
return 'Guest';
}
$this->setIdentity($auth->getIdentity());
}
return $this->_identity;
}
public function checkAcl($action)
{
return $this->getAcl()->isAllowed(
$this->getIdentity(),
$this,
$action
);
}
}

The SF_Model_Acl_Abstract class subclasses the SF_Model_Abstract and implements the SF_Model_Acl_Interface and Zend_Acl_Resource_Interface interfaces. All Models that need ACL support can now subclass the SF_Model_Acl_Abstract.

The setIdentity() method will accept either null, string, array, or a Zend_Acl_Role_Interface instance. The identity should contain the role to be used when checking the ACL. If no role is set, then we default to the Guest role.

The getIdentity() method is designed to lazy load the identity from Zend_Auth. Therefore, we first check if the $_identity property is null. If it is, then we retrieve the identity from Zend_Auth and set it on the Model using setIdentity(). The identity returned by Zend_Auth will be an instance of Storefront_Resource_User_Item. This implements the Zend_Acl_Role_Interfac and as a result it is fine to set it on the Model. During normal use we would always rely on the lazy loading here. The only time we would not is during testing when we need to set the identity ourselves and not from the session.

The checkAcl() method is used to query the ACL. This method simply returns the result of the isAllowed() method of the Zend_Acl class. We can see that we pass the identity, the resource ($this/$the Model), and the action to isAllowed(). The action will be defined by us when we configure the ACL inside the concrete Models and simply represent the permission or the action that is trying to be undergone.

You will notice that we still have not implemented some of the methods defined in the SF_Model_Acl_Interface and Zend_Acl_Resource_Interface interfaces. These will need to be implemented inside the concrete Models as they contain Model specific settings.

Zend Framework 1.8 Web Application Development Design, develop, and deploy feature-rich PHP web applications with this MVC framework
Published: September 2009
eBook Price: $23.99
Book Price: $39.99
See more
Select your format and quantity:

Securing the User Model

Now that we have the base class created, we can start securing our application. To do this, we will edit the User Model. The first step is to have the Model subclass the SF_Model_Acl_Abstract class.

application/modules/storefront/models/User.php

class Storefront_Model_User extends SF_Model_Acl_Abstract
{

Once we have the User Model subclassing the SF_Model_Acl_Abstract, we then must implement the getAcl() , setAcl(), and getResourceId() methods.

application/modules/storefront/models/User.php

public function getResourceId()
{
return 'User';
}
public function setAcl(SF_Acl_Interface $acl)
{
if (!$acl->has($this->getResourceId())) {
$acl->add($this)
->allow('Guest', $this, array('register'))
->allow('Customer', $this, array('saveUser'))
->allow('Admin', $this);
}
$this->_acl = $acl;
return $this;
}
public function getAcl()
{
if (null === $this->_acl) {
$this->setAcl(new Storefront_Model_Acl_Storefront());
}
return $this->_acl;
}

First we implement the getResourceId() method, which is defined by the Zend_Acl_Resource_Interface interface and simply returns the string identifying the resource (the Model) as User.

Next, we implement the setAcl() method, which is defined by the SF_Model_Acl_Interface interface. This method is responsible for configuring our ACL by adding the Resources and Rules. We first check to see if the ACL has the Resource registered to it. If not, we then add the Resource ($this) and configure the rules for the User Model. The rules here are as follows:

  • Guest can access register
  • Customer can access register and saveUser
  • Admin can access everything (we pass null as the action)

Once the ACL is configured, we set the ACL on the $_acl property and return $this to allow method chaining. Note that the permission names we use do not have to match method names.

Our final method is getAcl(). This again is defined by the SF_Model_Acl_Interface interface. This method checks if the $_acl property has been set and then if not sets a new Storefront_Model_Acl_Storefront instance as the ACL to be used. We do this to help with testing later on, as it allows us to not use the default ACL and inject a mock one instead.

Now that we have implemented all of our required methods, we can start querying the ACL to deny or allow access to parts of the Model. Edit the User Model and add the following to the methods as shown.

application/modules/storefront/models/User.php

public function saveUser($post, $validator = null)
{
if (!$this->checkAcl('saveUser')) {
throw new SF_Acl_Exception("Insufficient rights");
}
/*...*/
public function registerUser($post)
{
if (!$this->checkAcl('register')) {
throw new SF_Acl_Exception("Insufficient rights");
}
/*...*/

To query the ACL, we simply need to call the checkAcl() method. This will then query the ACL for us and tell us if the current user has the correct access permissions. If the User does not have the correct permissions, then we throw an SF_Acl_Exception. You will need to copy this exception class from the example files.

We now have a fully working ACL that is integrated into the Domain layer of our MVC application and we are not depending on the Controller layer for this functionality, meaning we can use the Models outside the MVC context. It is important to note that we could also implement the ACL in this way for other entities within the application, such as Services. All we need to do is create the base classes for that namespace or create a more generic set of ACL base classes.

Non-Model ACL

With this implementation, we also have the ability to use the ACL in a more common way. We can still add other Resources to the ACL that are not Models, meaning we can control access to non-Model Resources.

For the next step, we will be creating the administrator functionality for the Storefront in future. The way we do this there will not be an admin Model. However, we still want to deny access to this area to anyone without the Admin Role. To deal with this requirement, we are going to create a new Resource and add it to the ACL. To start, create the following ACL resource class:

application/modules/storefront/models/Acl/Resources/Admin.php

class Storefront_Model_Acl_Resource_Admin implements
Zend_Acl_Resource_Interface
{
public function getResourceId()
{
return 'Admin';
}
}

Here we have simply created a new ACL Resource identified as Admin, which will represent the administration area. Next, we need to add the following code to the ACL:

application/modules/storefront/models/Acl/Storefront.php

class Storefront_Model_Acl_Storefront extends Zend_Acl implements SF_Acl_Interface
{
public function __construct()
{
$this->addRole(new Storefront_Model_Acl_Role_Guest)
->addRole(new Storefront_Model_Acl_Role_Customer,
'Guest')
->addRole(new Storefront_Model_Acl_Role_Admin,
'Customer');
$this->deny();
$this->add(new Storefront_Model_Acl_Resource_Admin)
->allow('Admin');
}
}

Here we add the new Resource to the main ACL and allow admin access for all permissions. We now have the ACL configured and can query it to see if a user is allowed to access this Resource.

It is important to note that when we use the ACL like this it obviously creates a dependency on the application layer, so it is important that we only use it where necessary and make sure we push whatever we can into the Models. The Admin Resource only really exists within the Application layer and has no Model. This will become clearer when we implement the administration area.

Unit testing with ACL

One of the advantages of integrating the ACL into our Models is that we can now test security in our unit tests. There is a whole suite of unit tests included with the example files. Let's have a look at the User Model tests as well as one of the tests that makes use of the new ACL integration:

tests/unit/Model/UserTest.php

public function test_User_Can_Be_Edited_By_Customer_And_Admin()
{
$post = array(
'userId' => 10,
'title' => 'Mr',
'firstname' => 'keith',
'lastname' => 'pope',
'email' => 'me@me.com'
);
// Guest
try {
$edit = $this->_model->saveUser($post);
$this->fail('Guest should not be able to edit user');
} catch (SF_Acl_Exception $e) {}
// Customer
try {
$this->_model->setIdentity(array('role' => 'Customer'));
$edit = $this->_model->saveUser($post);
} catch (SF_Acl_Exception $e) {
$this->fail('Customer should be able to edit user');
}
// Admin
try {
$this->_model->setIdentity(array('role' => 'Admin'));
$edit = $this->_model->saveUser($post);
} catch (SF_Acl_Exception $e) {
$this->fail('Admin should be able to edit user');
}
$this->assertEquals(10, $edit);
}

This test is used to validate the security of the User Model without going into too much detail on how PHPUnit works. We can have three main assertions in this test:

  • Guest should not be able to saveUser
  • Customer should be able to saveUser
  • Admin should be able to saveUser

When we run the Guest assertion, we use the default identity created by our Models, which is Guest. This means that the $_model->saveUser() call should throw an SF_Acl_Exception. If it does not, then we fail the test.

When running the Customer and Admin assertions, we inject the role we wish to use by calling the setIdentity() method. Remember we have no Zend_Auth session, so we manually set the identity. We then fail the test if an SF_Acl_Exception is thrown as Customer and Admin should be allowed to saveUser.

As we can see, the Model-ACL implementation provides us with a flexible platform for testing and using the Models outside the MVC setting.

Summary

We now have a fully working authentication and authorization system integrated into the Storefront.

In this article, we looked at authorization (can they do this?) and different ways of using Zend_Acl. We opted to integrate the authorization layer into our Models so that we could use the Models outside the typical MVC context. To implement this, we created a new base Model class for Models that need ACL support and then secured the User Model using this new functionality.


If you have read this article you may be interested to view :


About the Author :


Books From Packt


Joomla! 1.5 SEO
Joomla! 1.5 SEO

jQuery 1.3 with PHP
jQuery 1.3 with PHP

Symfony 1.3 Web Application Development
Symfony 1.3 Web Application Development

Joomla! 1.5x Customization: Make Your Site Adapt to Your Needs
Joomla! 1.5x Customization: Make Your Site Adapt to Your Needs

PHP Team Development
PHP Team Development

Matplotlib for Python Developers
Matplotlib for Python Developers

Spring Web Flow 2 Web Development
Spring Web Flow 2 Web Development

Grok 1.0 Web Development
Grok 1.0 Web Development


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
v
3
D
d
S
P
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software