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.
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
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
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
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
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
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');
}
}
?>
|
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
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
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.
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
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.
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!
| Description | Name | Size | Download method |
|---|---|---|---|
| Archive of the example application in this article | example-app-rbac.zip | 3784KB | HTTP |
Information about download methods
Learn
- Five-part article series: Introduction
to MVC Programming with Agavi (Vikram Vaswani, developerWorks, July - September 2009): Learn
the basics of application development with Agavi using the five-part series:
- Part 1: Open a whole new world with Agavi: Learn to build scalable Web applications with the Agavi framework (July 2009): In the first of a five-part series, explore the basic concepts of the Agavi framework and compare it to other frameworks. Read all about views, actions, templates, and routes, and start to build your own scalable Agavi application.
- Part 2: Add forms and database support with Agavi and Doctrine (July 2009): Continue to explore the flexible, open-source Agavi framework as you create an input form, auto-generate data models with Doctrine, and integrate these models into the Agavi project
- Part 3: Add authentication and administrative functions with Agavi (August 2009): Build more function into the sample Web Automobile Sales Platform by adding the ability to add, delete, and update the automobile records. You will also see how to separate user functions from administrative functions with authentication.
- Part 4: Create an Agavi search engine with multiple output types including XML, RSS, or SOAP (August 2009): Implement a simple search engine and add support for multiple output types such as XML, RSS, or SOAP for your sample Agavi program.
- Part 5: Add paging, file uploads, and custom input validators to your Agavi application (September 2009): Polish your sample Agavi application to support file uploads, store user data in sessions, integrate third-party libraries, and create custom input validators.
- The official Agavi Web site and the Agavi Guide: Learn more about this scalable PHP5 application framework that follows the MVC paradigm.
- The Agavi API documentation:Take a closer look at the Agavi base classes.
- The Agavi cookbook: Find out how to perform common tasks.
- Doctrine ORM for PHP - Introduction to Models: Understand how to generate and use Doctrine models.
- Doctrine
ORM for PHP - Working with Models: View sample queries using Doctrine.
- The Agavi blog: Read Agavi news.
- The Agavi mailing lists and IRC channels: Participate in the Agavi community, ask questions and get answers.
- The XML area on developerWorks: Get the resources you need to advance your skills in the XML arena.
- IBM XML certification: Find out how you can become an IBM-Certified Developer in XML and related technologies.
- XML technical library: See the developerWorks XML Zone for a wide range of technical articles and tips, tutorials, standards, and IBM Redbooks.
- developerWorks technical events and webcasts: Stay current with technology in these sessions.
- developerWorks podcasts: Listen to interesting interviews and discussions for software developers.
Get products and technologies
- The Agavi framework: Download this PHP5-MVC pattern application framework for cleaner extensible code.
- The MySQL database server: Download a popular open source database.
- The Doctrine ORM package: Download this object relational mapper for PHP.
- IBM product evaluation versions: Download or explore the online trials in the IBM SOA Sandbox and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
- XML zone discussion forums: Participate in any of several XML-related discussions.
- developerWorks blogs: Check out these blogs and get involved in the developerWorks community.

Vikram 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.



