Implement access control with Agavi

Learn to build scalable Web applications with the Agavi framework

Agavi is an open-source, flexible, and scalable framework for application development. One of its key features is a full-featured API for user authentication and role-based access control. Examine this API in detail, and see how to add sophisticated application-level privilege management and manipulation to a Web application.

Vikram Vaswani, Founder, Melonfire

Photo of Vikram VaswaniVikram Vaswani is the founder and CEO of Melonfire, a consulting services firm with special expertise in open-source tools and technologies. He is also the author of the books PHP Programming Solutions and PHP: A Beginners Guide.



27 October 2009

Also available in Japanese Vietnamese

Introduction

In a previous set of articles, I introduced you to the Agavi MVC framework, illustrating how you can use it to quickly and efficiently build a scalable Web application. One of my key reasons for selecting Agavi as the development framework was its sophisticated input filtering and validation system, which ensures that invalid or unfiltered input never makes it past the front door of your application. This task, a key element of building a secure Web application, is made significantly simpler by Agavi's built-in validators for strings, numbers, timestamps, e-mail addresses and files, as well as its support for custom validators.

Agavi's focus on application security doesn't end with input validation. The framework also exposes a powerful user authentication and access control subsystem that you can customize it to meet the requirements of almost any Web application. This subsystem supports both simple login-based authentication and more complex role-based access control (RBAC), and it provides a solid foundation for application-level privilege management and manipulation. I will discuss it in more detail in this article.


Understanding basic concepts

Frequently used acronyms

  • API: Application program interface
  • MVC: Model-View-Controller
  • XML: Extensible Markup Language

When you sit down to define access rules to actions within an Agavi application, you must first realize that there are different levels of security available. Broadly, these levels can be described using the following concepts:

  • Passwords. Password-based access is the simplest type of access control. Fundamentally, it allows the developer to mark certain actions as secure, and requires the user to enter a valid set of login credentials before granting access to that action. This type of access control is suitable for applications which don't require multiple levels of privilege, or where the users accessing the system can broadly be classified into users and administrators.

  • Privileges. Privilege-based access control is a more granular system as compared to password-based access control. Under this system, the developer defines the privileges required to perform each action, and the system allows access to those actions only if the user requesting access possesses the necessary privileges. This approach embodies both authentication and authorization. Users need not only a valid set of login credentials, but an accompanying set of privileges, in order to perform particular actions. However, it can quickly become unmanageable as the numbers of user types and privilege levels increase.

  • Roles. Role-based access control (RBAC) is a more sophisticated and maintainable version of the privilege-based approach described previously. This approach defines a set of user roles for an application; each role embodies a set of privileges, and application users are assigned one or more roles depending on the functions they need to perform. This type of access control is suitable for applications with multiple types of users and multiple privilege levels. And it is flexible enough to accommodate a diverse variety of authentication and authorization needs.


Setting up the example application

Before you can start to implement access control, a few notes. Throughout this article, I'll assume that you have a working Apache/PHP/MySQL development environment, and that you're familiar with the basics of SQL and XML. I'll also assume that you're conversant with:

  • The basic principles of application development with Agavi
  • Understand the interaction between actions, views, models and routes
  • Are familiar with using Doctrine models in an Agavi application

In case you're not familiar with these topics, you should read the introductory Agavi article series (see Resources for a link) before proceeding with this article.

Step 1: Initialize a new application

To begin, you'll first set up a simple Agavi application that will serve as a testbed for the development goals of this article. Use the Agavi build script to initialize a new project, accepting default values except where shown below:

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) []: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

While you're at it, define a new virtual host for your test application, such as http://example.localhost/, in your Apache configuration, and then point your browser to it. You should see the default Agavi welcome page, as in Figure 1.

Figure 1. The default Agavi application welcome page
Screen capture of the default Agavi application welcome page

Step 2: Add a new module and corresponding actions

For simplicity, I assume that all the actions you wish to protect are located in a module other than the Default module. Drop back to your command prompt and create a new Book module containing six actions using the Agavi build script, as below:

shell> agavi module-wizard
Module name: Book
Space-separated list of actions to create for Book: 
  Create Index Delete Display Search Edit
...

These six actions—CreateAction, DeleteAction, DisplayAction, IndexAction, EditAction, and SearchAction—are the actions to which you'll shortly be adding access control. For the moment, update each action's *Success template with a brief message indicating its purpose (where * is the name of the action). Here's an example of what the CreateSuccess template might contain:

If you can see this page, you are authorized to create and add new books to the database.

At this point, you should also remove the Welcome module, as recommended by the Agavi documentation.

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

Step 3: Update the application routing table

Finally, update the application's routing table, at $ROOT/app/config/routing.xml, with additional routes pointing to the new actions, as in Listing 1.

Listing 1. The example application's routes
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />
      
      <!-- action for book pages "/book/*" -->
      <route name="book" pattern="^/book" module="Book">
        <route name=".index" pattern="^/index$" action="Index" />
        <route name=".create" pattern="^/create$" action="Create" />
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
        <route name=".edit" pattern="^/edit/(id:\d+)$" action="Edit" />
        <route name=".delete" pattern="^/delete/(id:\d+)$" action="Delete" />
        <route name=".search" pattern="^/search$" action="Search" />
      </route>
            
    </routes>
  </ae:configuration>
</ae:configurations>

You should now be able to access the newly-minted actions using the routes in Listing 1. To verify this, try browsing to http://example.localhost/book/create and confirm that you are presented with a page similar to that in Figure 2.

Figure 2. The default page for the CreateAction
Screen capture of the default pagef for the CreateSuccess template

In a similar manner, verify the other routes in Listing 1 before you proceed to the next section. In case you're not able to get things working, remember that the steps above are described in more detail in Part 1 of the introductory Agavi series (see Resources for a link). Alternatively, you can download a complete code archive of the example application from Downloads in this article.


Setting up the login and logout actions

Regardless of whether your application's access control will be based on passwords or roles, you'll still need a login and logout system to handle user authentication. Therefore, once the basic application has been created, the next step is to implement login and logout actions.

Step 1: Initialize the user database and model

Since user information for Web applications is commonly stored in a database, that's what you'll do here as well. To begin, create a new MySQL table to hold user credentials, as below:

mysql> CREATE TABLE IF NOT EXISTS `user` (
    -> UserID int(4) NOT NULL AUTO_INCREMENT,
    -> Username varchar(50) CHARACTER SET utf8 NOT NULL,
    -> `Password` text CHARACTER SET utf8 NOT NULL,
    -> PRIMARY KEY (UserID),
    -> UNIQUE KEY Username (Username)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Query OK, 0 rows affected (0.13 sec)

Seed this table with a few accounts to get things rolling:

mysql> INSERT INTO user (UserID, Username, Password) 
      VALUES(1, 'james', PASSWORD('james'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user (UserID, Username, Password) 
      VALUES(2, 'susan', PASSWORD('susan'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user (UserID, Username, Password) 
      VALUES(3, 'marco', PASSWORD('marco'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user (UserID, Username, Password) 
      VALUES(4, 'donald', PASSWORD('donald'));
Query OK, 1 row affected (0.08 sec)

You can also create a table to hold privilege information, although you'll use it a little further along:

mysql> CREATE TABLE IF NOT EXISTS `user_access` (
    -> RecordID int(4) NOT NULL AUTO_INCREMENT,
    -> UserID int(4) NOT NULL,
    -> UserAccess varchar(255) NOT NULL,
    -> PRIMARY KEY (RecordID),
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.1 sec)

Then, download the Doctrine ORM (see Resources for a link) and add the Doctrine libraries to $ROOT/libs/doctrine. You must also update your application settings, in $ROOT/app/config/settings.xml, to activate database support, and then update your database configuration file, typically found at $ROOT/app/config/databases.xml, to use the Doctrine adapter in Agavi. Listing 2 has an example of what this configuration might look like:

Listing 2. Agavi Doctrine adapter configuration
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">
  <ae:configuration>
    <databases default="doctrine">    
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">
         mysql://user:pass@localhost/example
       </ae:parameter>
        <ae:parameter name="load_models">
         %core.lib_dir%/doctrine
       </ae:parameter>
      </database>      
    </databases>
  </ae:configuration>
</ae:configurations>

At this point, you can use Doctrine to generate models for these tables. Remember to manually copy the resulting model classes to your $ROOT/app/lib/doctrine/ directory.

shell> cp /tmp/models/User.php app/lib/doctrine/
shell> cp /tmp/models/generated/BaseUser.php app/lib/doctrine/
shell> cp /tmp/models/UserAccess.php app/lib/doctrine/
shell> cp /tmp/models/generated/BaseUserAccess.php app/lib/doctrine/

The process of integrating Doctrine with Agavi and using it to generate models from database tables is discussed in detail in Part 3 of the introductory Agavi series (see Resources of this article for a link).

Step 2: Add the various login and logout views

Once you have the model working, the next step is to add a LoginAction and a LogoutAction. By default, the Agavi build script will have created a LoginAction at project creation time, so all you need is a LogoutAction.

shell> agavi action-wizard
Module name: Default
Action name: Logout
Space-separated list of views to create for Save [Success]: Success
...

You'll also need to generate templates for the LoginInputView, LoginSuccessView and LoginErrorView, as below:

shell> agavi template-create
Module name: Default
Template name: LoginSuccess
...

shell> agavi template-create
Module name: Default
Template name: LoginInput
...

shell> agavi template-create
Module name: Default
Template name: LoginError
...

Of these views, the LoginInputView is probably the most important. It is responsible for generating the login form that users will see when they try to access a restricted action. It's also responsible for storing the original request URL and redirecting the user to that URL after successful login. According to the Agavi cookbook (look in Resources for a link), the easiest way to do this is to store the original request URL in the execution context, as in Listing 3:

Listing 3. The LoginInputView definition
<?php

class Default_LoginInputView extends ExampleAppDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    // get referrer URL and save it
    if($this->getContainer()->hasAttributeNamespace(
      'org.agavi.controller.forwards.login')) {
      $this->getContext()->getUser()->setAttribute(
       'redirect', $this->getContext()->getRequest()->getUrl(), 
       'org.agavi.example.login');
    } else {
      $this->getContext()->getUser()->removeAttribute(
        'redirect', 'org.agavi.example.login');
    }     
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>

Listing 4 has the code for the corresponding LoginInput template.

Listing 4. The LoginInput template
<form action="<?php echo $ro->gen('login'); ?>" method="post">
  <label for="username" class="required">Username:</label>
  <br/>
  <input id="username" type="text" name="username" style="width:150px" />
  <p/>
  <label for="password" class="required">Password:</label>
  <br/>
  <input id="password" type="password" name="password" style="width:150px" />
  <p/>
  <input type="submit" name="submit" class="submit" value="Log In" />
</form>

As you might expect, this is fairly standard: a form with fields for username and password. Figure 3 illustrates what it looks like.

Figure 3. The application login page
Screen capture of the application login page with Username and Password fields

Listing 5 has the input validation rules for the form in Listing 4:

Listing 5. The LoginAction validator
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Default/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>username</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Username is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="string">
        <arguments>
          <argument>password</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Password is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>
    
  </ae:configuration>
</ae:configurations>

Assuming a successful login, the LoginSuccessView retrieves the original URL request and redirects the client to it (Listing 6):

Listing 6. The LoginSuccessView definition
<?php

class Default_LoginSuccessView extends ExampleAppDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    if($this->getContext()->getUser()->hasAttribute(
      'redirect', 'org.agavi.example.login')) {
      $this->getResponse()->setRedirect($this->getContext()
        ->getUser()->removeAttribute(
        'redirect', 'org.agavi.example.login'));
      return true;
    }
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>

If there is no redirection to be performed, the LoginSuccessView simply renders the LoginSuccess template, which contains a login confirmation message (Listing 7).

Listing 7. The LoginSuccess template
You were successfully logged in.

Alternatively, if the login operation fails, the LoginError template will be rendered (Listing 8):

Listing 8. The LoginError template
There was an error logging you in. Please try again.

Step 3: Implement the login and logout actions

You're now ready to add some code to the LoginAction. Listing 9 illustrates one such LoginAction, which is responsible for reading user credentials submitted through the LoginInput form in Listing 4, and validate them against the information stored in the MySQL database. If valid, the LoginAction uses the setAuthenticated() method to set an authentication flag and renders the LoginSuccessView; if not, it returns the LoginErrorView.

Listing 9. The LoginAction definition
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

The LogoutAction does the reverse: it resets the user authentication flag and ends the user session. Listing 10 has the code:

Listing 10. The LogoutAction definition
<?php

class Default_LogoutAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead()
  {
    $this->getContext()->getUser()->setAuthenticated(false);
    return 'Success';
  }
}

?>

Step 4: Update the application's routing table

The final step is to update the application's routing table with additional routes for the login and logout actions. Listing 11 displays the updated routes:

Listing 11. The example application's updated routes
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />
      
      <!-- action for login page "/login" -->
      <route name="login" pattern="^/login$" module="Default" action="Login" />
      
      <!-- action for admin logout pages "/logout" -->
      <route name="logout" pattern="^/logout$" module="Default" action="Logout" />
		 		 		       
      <!-- action for book pages "/book/*" -->
      <route name="book" pattern="^/book" module="Book">
        <route name=".index" pattern="^/index$" action="Index" />
        <route name=".create" pattern="^/create$" action="Create" />
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
        <route name=".edit" pattern="^/edit/(id:\d+)$" action="Edit" />
        <route name=".delete" pattern="^/delete/(id:\d+)$" action="Delete" />
        <route name=".search" pattern="^/search$" action="Search" />
      </route>            
    </routes>
  </ae:configuration>
</ae:configurations>

At this point, you have a working login and logout system. Try it out for yourself by visiting http://example.localhost/login and entering credentials matching those set earlier in the user table. If all goes well, you should see a login success message, as in Figure 4.

Figure 4. The result of a successful login
Screen capture showing the result of a successful login with 'You were successfully logged in.' message

If you enter an incorrect username or password, you should see a login error message, as in Figure 5.

Figure 5. The result of an unsuccessful login
Screen capture with result of an unsuccessful login with 'There was an error logging in. Please try again.' message

Controlling access with passwords

Once you have a working login and logout framework, it's extremely easy to restrict access to certain actions. To illustrate, assume that all the actions in the Book module require authentication. To enable this, simply edit each action class and add an isSecure() method which returns true. Listing 12 has an example of what the revised CreateAction will look like:

Listing 12. The CreateAction definition
<?php

class Book_CreateAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
}

?>

And now, when you try to access any of these actions—for example, the CreateAction at http://example.localhost/book/create—Agavi will first redirect you to the LoginAction and only display the requested URL once you enter a valid username and password.

It's important to note that after you log in to access any one action, you can also access the remaining actions without encountering any further login prompts. This is important because it illustrates one of the key flaws of this simple, password-only approach—namely that, once authenticated, a user can access any or all of the protected actions. In other words, this simple approach fails to distinguish between different types of users, making it unsuitable for applications that require granular access control. And that's where the privilege-based approach comes in.


Controlling access with privileges

Under a privilege-based approach, each action requires certain privileges and each authenticated user possesses certain privileges. Actions can only be executed by users who possess the necessary privileges for that action. This allows actions to be restricted on a per-user basis, allowing fine-grained control over which user can access which action.

Step 1: Set required action privileges

To illustrate how this works, assume the following restrictions on actions:

  • The CreateAction and DeleteAction can only be invoked by users with the 'book.create' privilege.
  • The EditAction can only be invoked by users with the 'book.edit' privilege.
  • The IndexAction can only be invoked by users with the 'book.index' privilege.
  • The DisplayAction can only be invoked by users with the 'book.display' privilege.
  • The SearchAction can only be invoked by users with both the 'book.index' and 'book.display' privileges.

Within an action, these privileges are set using the action's getCredentials() method, which returns the privileges needed to use it. Consider Listing 13, which illustrates the revised CreateAction:

Listing 13. The CreateAction definition
<?php

class Book_CreateAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  }
  
  public function getCredentials()
  {
    return 'book.create';
  } 
}

?>

Similarly, Listing 14 updates the IndexAction to reflect that only users with the 'book.index' privilege can access it:

Listing 14. The IndexAction definition
<?php

class Book_IndexAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
  
  public function getCredentials()
  {
    return 'book.index';    
  }   
}

?>

Listing 15 updates the SearchAction to illustrate that two privileges, 'book.index' and 'book.display', are required to access it, by having the getCredentials() method return an array:

Listing 15. The SearchAction definition
<?php

class Book_SearchAction extends ExampleAppBookBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function isSecure()
  {
    return true;
  } 
  
  public function getCredentials()
  {
    return array('book.index', 'book.display');    
  }   
  
}

?>

Step 2: Set user privileges

Once you have your actions configured, the next step is to assign privileges to users. Assume that:

  • User 'james' has the 'book.index' privilege
  • User 'susan' has the 'book.index' and 'book.display' privileges
  • User 'marco' has the 'book.edit' and 'book.display' privileges
  • User 'donald' has the 'book.index', 'book.display' and 'book.create' privileges

Add these privileges to the MySQL database using the following SQL:

mysql> INSERT INTO user_access (UserID, UserAccess) VALUES
    -> (1, 'book.index'),
    -> (2, 'book.index'),
    -> (2, 'book.display'),
    -> (3, 'book.display'),
    -> (3, 'book.edit'),
    -> (4, 'book.index'),
    -> (4, 'book.display'),
    -> (4, 'book.create');
Query OK, 8 rows affected (0.05 sec)
Records: 8  Duplicates: 0  Warnings: 0

Figure 6 illustrates the relationship between users and privileges in the MySQL database. (View a text version of Figure 6.)

Figure 6. The user-privilege map
Screen capture of the user-privilege map

Step 3: Retrieve and assign user privileges at run-time

The final step is to update the LoginAction to retrieve the privileges of each user from the database at login-time, and assign them to the user object. Listing 16 contains the updated code:

Listing 16. The updated LoginAction definition
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);

      // get credentials and attach to user object
      $this->getContext()->getUser()->clearCredentials();      
      $q = Doctrine_Query::create()
            ->from('UserAccess ua')
            ->where('ua.UserID = ?', array($result[0]['UserID']));
      $rs = $q->fetchArray();
      foreach ($rs as $r) {
        $this->getContext()->getUser()->addCredential(trim($r['UserAccess']));        
      }
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

Here, once the login credentials of a user are verified, an additional query is performed to retrieve his or her privileges. These privileges are then attached to the user object with the addCredential() method, and the LoginSuccessView is rendered. Notice also the clearCredentials() method, which is used to clear all user credentials. To maximize security, this should be done before every operation that involves a change in credentials, as well as when the user logs out.

To see this in action, try logging in as james. Once logged-in, you should be able to access the IndexAction. However, any attempt to access the other actions will be met with an Access Denied response, as in Figure 7.

Figure 7. The result of attempting to access a privileged action
Screen capture after an attempt to access a privileged action shows an insufficient credentials error message

If you log in as marco, on the other hand, you should be able to access the DisplayAction and the EditAction, but not the other actions. If you log in as susan, you should have access to the IndexAction, DisplayAction and SearchAction. And if you log in as donald, you should have access to the IndexAction, DisplayAction, CreateAction, DeleteAction and SearchAction.

This approach obviously offers more precise control than the simple password-based authentication shown earlier, and is recommended for applications that require multiple levels of access control. However, as the numbers of privilege levels and users increase, maintaining the user-privilege mapping becomes increasingly complex and time-consuming. And that's where roles come in.


Implementing role-based access control

RBAC is a popular technique used to handle authorization tasks in a multi-user, multi-privilege application. Essentially, it allows you to define user roles, each of which embodies a set of pre-defined privileges, and assign these roles to application users. When a user logs in, he or she is associated with one or more roles, and automatically obtains all the privileges that come with the role(s).

Step 1: Define roles and privileges

Like most other things in Agavi, roles and privileges are defined in an XML configuration file, located by default at $ROOT/app/config/rbac_definitions.xml. The definitions file is automatically read by the AgaviRbacSecurityUser object. Listing 17 has an example of what it looks like:

Listing 17. The role definitions in the example application
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
  xmlns="http://agavi.org/agavi/config/parts/rbac_definitions/1.0">
  <ae:configuration>
    <roles>
      <role name="visitor">
        <permissions>
          <permission>book.index</permission>
        </permissions>
        <roles>
          <role name="student">
            <permissions>
              <permission>book.display</permission>
            </permissions>
            <roles>
              <role name="manager">
                <permissions>
                  <permission>book.create</permission>
                </permissions>
              </role>
            </roles>
          </role>
        </roles>
      </role>
      <role name="librarian">
        <permissions>
          <permission>book.display</permission>
          <permission>book.edit</permission>
        </permissions>
      </role>           
    </roles>
  </ae:configuration>
</ae:configurations>

This file defines four roles: manager, librarian, student, and visitor. Each of these roles is associated with different privileges. As the structure of the XML file illustrates, roles can be nested: A child role inherits the privileges of its parent. Therefore, a manager gains both its own custom privileges, as well as the privileges of student and visitor.

Step 2: Assign roles to users

The next step is to assign roles to users. Assume that:

  • User james is a visitor.
  • User susan is a student.
  • User marco is a librarian.
  • User donald is a manager.

You can reuse the existing MySQL table to assign roles to each user, as shown below:

mysql> TRUNCATE TABLE user_access;
Query OK, 0 rows affected (0.06 sec)

mysql> INSERT INTO user_access (UserID, UserAccess) VALUES
    -> (1, 'visitor'),
    -> (2, 'student'),
    -> (3, 'librarian'),
    -> (4, 'manager');
Query OK, 4 rows affected (0.05 sec)
Records: 4  Duplicates: 0  Warnings: 0

Figure 8 illustrates the relationship between users and roles in the MySQL database. (View a text-only version of Figure 8.)

Figure 8. The user-role map
Screen capture of the user-role map

Step 3: Instantiate the AgaviRbacSecurityUser object

By default, Agavi represents application users with the AgaviSecurityUser object. However, this object lacks the methods needed for run-time role assignment and revocation. Therefore, you must tell Agavi to instantiate AgaviRbacSecurityUser objects instead of AgaviSecurityUser objects, by editing $ROOT/app/config/factories.xml and updating the class factory list as in Listing 18:

Listing 18. The Agavi class factory configuration
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
  xmlns="http://agavi.org/agavi/config/parts/factories/1.0">
  
  <ae:configuration>    
    ...
    <user class="AgaviRbacSecurityUser" />
    ...
  </ae:configuration>
  
</ae:configurations>

Step 4: Retrieve and assign user roles at run-time

The final step is to update the LoginAction to retrieve the roles for each user from the database once successfully authenticated, and assign these roles to the user object. Listing 19 has the updated code for the LoginAction:

Listing 19. The updated LoginAction definition
<?php

class Default_LoginAction extends ExampleAppDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    // get input parameters
    $u = $rd->getParameter('username');
    $p = $rd->getParameter('password');
    
    // check user credentials
    $q = Doctrine_Query::create()
          ->from('User u')
          ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
    $result = $q->fetchArray();
      
    // set authentication flag if valid
    if (count($result) == 1) {
      $this->getContext()->getUser()->setAuthenticated(true);
      // get and grant roles
      $this->getContext()->getUser()->revokeAllRoles();        
      $q = Doctrine_Query::create()
            ->from('UserAccess ua')
            ->where('ua.UserID = ?', array($result[0]['UserID']));
      $rs = $q->fetchArray();
      foreach ($rs as $r) {
        $this->getContext()->getUser()->grantRole(trim($r['UserAccess']));        
      }
      return 'Success';
    } else {
      return 'Error';
    }
  } 
}

?>

Once the login credentials of a user are verified, an additional query is performed to retrieve his or her roles. These roles are then attached to the AgaviRbacSecurityUser object with the grantRole() method, and the LoginSuccessView is rendered. Notice also the revokeAllRoles() method, which is used to clear all the user's existing roles before granting new roles, as well as when the user logs out.

If you log in as james, you should be able to access the IndexAction only. If you log in as marco, on the other hand, you should be able to access the DisplayAction and the EditAction, but not the other actions. If you log in as susan, you should have access to the IndexAction, DisplayAction and SearchAction. And if you log in as donald, you should have access to the IndexAction, DisplayAction, CreateAction, DeleteAction and SearchAction.

Because a single role can embody multiple privileges, and a single user can have multiple roles, Agavi's RBAC implementation makes it easy to create a multi-level privilege hierarchy. Maintenance is also significantly simpler, as you can change the privileges applicable to each role, and have those changes automatically cascaded to all users in those roles, simply by editing the role definitions XML file. This makes it ideal for Web applications that have a high degree of complexity, and that require fine-grained control over which user has access to which function. And, obviously, you can (and should) extend the base AgaviRbacSecurityUser object with additional methods related to your own user management requirements.


Conclusion

It should be clear from the preceding discussion that Agavi's access control mechanism has something for everyone. Regardless of whether you're looking for a flat or hierarchical privilege system, Agavi's built-in objects make it easy to implement a secure, robust architecture and protect access to application functions in a simple and elegant manner. The modular nature of Agavi's access control implementation also means that you can add access control to your application at any time during the implementation phase, or even after the application has been deployed, with minimal impact on existing business logic.

The Downloads section has all the code implemented in this article. I recommend you get it, start playing with it, and maybe try your hand at adding new things to it. I guarantee you won't break anything, and it will definitely add to your learning. Have fun!


Download

DescriptionNameSize
Archive of the example application in this articleexample-app-rbac.zip3784KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Open source
ArticleID=438941
ArticleTitle=Implement access control with Agavi
publish-date=10272009