Tutorial: Role-Based Access Control with CakePHP

In an earlier post I talked about access control with CakePHP‘s AclComponent and how it might be used for the simple-yet-powerful roled-based access control, where users can have multiple roles and permission checks are reduced to verifying if the user has a certain role. In this tutorial we’ll add role based access to the CakeBB sample app. It is a basic forum system where you have several forums, each forum has several topics, and topics have at least one post. Users can create forums, start new topics, post replies to topics and so on.

Currently CakeBB has no concept of users, which means that anyone can do anything and everyone’s posts show up as by the “Guest” user. To fix that we’ll add a user model, require that users log in for any action that’s not just reading, and enforce roles.

About AuthComponent

What is “auth,” anyway? Well it turns out that there are two kinds: authentication and authorization, and it’s important to make a clear distinction. Both are needed to build a secure system.

Authentication is making sure that you are who you say you are. Like when they ask to see your passport when boarding a flight on an airport. They want to be sure it’s really you and not some random guy that stole your boarding pass and says he’s you.

Authorization is making sure that you are allowed to do what you are about to do. Like when they ask to see you boarding pass when boarding a flight. They want to be sure you’re actually on that flight.

CakePHP’s AuthComponent does both. It checks authentication against a database table, username-and-password-style. It can check authorization in five different ways. It’s actually really easy to use, but the API may seem confusing because there is no high level distinction between authentication and authorization.

Adding authentication to CakeBB

Download CakeBB here or get the latest version from github. I assume you already have done some stuff with CakePHP so I won’t walk through setting up mod_rewrite or configuring a database. The CakePHP book already does a good job with it.

Create the database tables for CakeBB with app/config/schema/cakebb.sql.

Adding a user model

In a real forum you’d probably store all kinds of information about the users: email address, time of last activity, IP address and so on. Here we’ll get by with just the user name and password. Let’s go and create the the Cake model class and the database table:

<?php
// app/models/user.php
class Users extends AppModel {
    var $name = 'User';
    var $hasMany = array (
        'Topics' => array ('className'=>'Topic'),
        'Posts' => array ('className'=>'Post'),
    );
}
?>
CREATE TABLE users (
  id INTEGER NOT NULL AUTO_INCREMENT,
  username VARCHAR(60),
  password CHAR(40),
  PRIMARY KEY(id),
  UNIQUE INDEX(username)
)

Why CHAR(40) for password? Because AuthComponent generates a 40-character hash code and uses it instead of the plain password.

While we’re in the database, let’s add the user_id column to the topics and posts, so we have some way of knowing who wrote them.

ALTER TABLE topics ADD COLUMN user_id INTEGER;
ALTER TABLE posts ADD COLUMN user_id INTEGER;

Also edit the Post and Topic models to add the “belongs to User” relationship. Below an example for the Post model.

// app/models/post.php
    var $belongsTo = array (
	'Topic' => array (
	    'className' => 'Topic',
	),
	'User' => array (
	    'className' => 'User',
	),
    );

Requiring users to log in

Let’s first secure the application. Cake’s AppController class is perfect for that: what ever you put there is inherited by all the other controllers since it’s the super class. Create app/app_controller.php, it should look like this:

<!-- app/app_controller.php -->
<?php
class AppController extends Controller {
    var $components = array ('Auth', 'Acl', 'Session');

    function beforeFilter() {
        // This callback is executed before the action
        // don't require logging in for index and view actions
        $this->Auth->allow('index', 'view');
    }
}
?>

You can now browse the existing forums and read posts. For anything else you are redirected to /users/login. There you are greeted with a big red error message telling you that the users controller doesn’t exist.

So let’s create users controller in app/controllers/users_controller.php. The users controller must have methods for logging in and out:

<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $scaffold;

    function login() {
	// implemented by AuthComponent
    }
    function logout() {
	$this->redirect($this->Auth->logout());
    }
}
?>

The “login” method can be left empty because AuthComponent verifies the password. The “logout” method logs the user out by calling Auth->logout and redirects the user to another page. Thanks to cakephp’s scaffolding we get a basic admin interface for free.

Storing the user id in posts and topics

If you now try to post a topic or a reply it still shows up by Guest. This is because the user id is not being stored. To fix it we have to edit the add action in TopicsController, and the reply action in PostsController. We modify the posted form data to include the missing user_id field.

// app/controllers/topics_controller.php
    function add($forumId) {
	if (!empty($this->data)) {
	    $this->data['Topic']['user_id'] = $this->Auth->user('id');
	    $this->data['Posts'][0]['user_id'] = $this->Auth->user('id');
	    if ($this->Topic->saveAll($this->data)) {
	        ...
// app/controllers/posts_controller.php
    function reply($id = null) {
	$this->autoRender = false;
	if ($this->data) {
	    $this->data['Post']['user_id'] = $this->Auth->user('id');
	    if ($this->Post->save($this->data)) {
	        ...

As you can see, the id of the current user can be accessed through AuthComponent.

View and layouts

We’re still missing a basic login page where you could actually type in their username and password. Place this into app/views/users/login.ctp:

<h2>Log in</h2>

<?php
echo $form->create('User');
echo $form->input('username');
echo $form->input('password');
echo $form->end();
?>

If you now try logging in you are sent back to the log in page with now error message. It’s not surprising that it doesn’t work, we have yet to create a user account. But we really should show error messages. So open app/views/layouts/default.ctp, find the div with id=”content” and add and echo for the “auth” flash message:

<div id="content">
    <?php echo $this->Session->flash('auth'); ?>
    <?php echo $this->Session->flash(); ?>
    <?php echo $content_for_layout; ?>
</div>

Creating the first user account

To create the first user account

  1. pick a username and password and
  2. try to log in.

Ignore the error message, go to the SQL debug output in the bottom of the page. Copy the hashed password from the query that failed to return rows, and add the user to the users table manually. For example if you picked user “foo” and password “bar”:

INSERT INTO users (username, password) VALUES ('foo',
    'c1f5bd1d0e4cd2321e5cc6752a87676513fccff6');

Now you should be able to log in to the application and use everything as before.

Creating roles

We’ll establish three kinds of user roles: admins, mods, and regular users. Like in the earlier post, let’s assume the admin role doesn’t automatically grant the mod role’s permissions just to make things more interesting. The easiest way to create the role structure is with Cake’s AclShell. Open a terminal, go to the application directory, and give the following commands:

cake/console/cake acl initdb
cake/console/cake acl create aco / forum
cake/console/cake acl create aco forum admin
cake/console/cake acl create aco forum mod
cake/console/cake acl create aco mod user

Separating the roles from the root–like here with the “forum” node–may be a good idea. Consider the possibility that in the future you may need to add a new major feature to your application with access controlled through a new role hierarchy. The ACO/role tree now looks like this:

cake/console/cake acl view aco
Aco tree:
---------------------------------------------------------------
  [1] forum
    [2] admin
    [3] mod
      [4] user
---------------------------------------------------------------

Granting access to roles in AclShell

To bootstrap working with ACL you’ll have to create the ARO for a user and assign permissions manually. To make things not fail in the beginning we’ll give ourselves all the forum permissions. Assuming you’ve already created a user for yourself and it has id 1, give the following commands:

cake/console/cake acl create aro / forum-users
cake/console/cake acl grant forum-users user all
cake/console/cake acl create aro forum-users User.1
cake/console/cake acl grant User.1 forum all

Like with the roles, we are separating the users from the root in case later you need to do things differently.

What about the users that we create through the application, don’t they need AROs as well? They do, and it can be automated with Cake’s ACL behaviour. Just add the following snippet to the User model class.

// app/models/user.php
    var $actsAs = array('Acl' => array ('type'=>'requester'));
    function parentNode() {
        return 'forum-users';
    }

I find the ARO tree structure less useful than the ACO tree but I guess it makes sense if you have a lot of users and want to assign default roles based on the users’ department or group. Now that we have the roles in place and one role assigned to a user, let’s see how we would program this beast.

Checking the user is authorized

Like I said above, AuthComponent has five ways to check authorization. Which method is used is controlled by the $authorize member variable. Here we set $authorize to ‘controller’ in AppController’s beforeFilter, which means that AuthComponent will call the controller’s isAuthorized

// app/app_controller.php
    function beforeFilter() {
        // don't require logging in for index and view actions
        $this->Auth->allow('index', 'view');
        // check authorization by calling isAuthorized
        $this->Auth->authorize = 'controller';
    }

(If you are wondering about the four other values you can use: Long story short, if you set it to the string controller, model, or object, AuthComponent delegates the check to a method called “isAuthorized” of another object, which can be a controller, a model, or an arbitrary PHP object. The other two options are “crud” and “actions.” If you use them AuthComponent will assume the actions or crud are defined as ACOs and check access with AclComponent.)

Implementing isAuthorized

OK, now we want to make sure that the forum users don’t do anything their roles don’t permit. For example, we require the user has the admin role to add, edit, or delete forums and users. This is done by adding the following snippet.

// app/controllers/forums_controller.php, users_controller.php
    function isAuthorized() {
	$userId = $this->Auth->user('id');
	$aro = array('model' => 'User', 'foreign_key' => $userId);
	return $this->Acl->check($aro, 'forum/admin', '*');
    }

The index and view actions will still be accessible because we allow them in the AppController. As you can see the access check is handled with the Acl component.

The case for topics and posts is a little different because we want normal users to be able to modify or delete only their own posts, while users with the mod role can edit and delete any posts. This means that for the edit and delete actions we need to check whose post it is, like below.

// app/controllers/topics_controller.php
    function isAuthorized() {
	$userId = $this->Auth->user('id');
	$aro = array('model' => 'User', 'foreign_key' => $userId);
	// Require "mod" role for editing other's posts
	$role = 'forum/mod/user';
	switch ($this->action) {
	case 'delete':
	case 'edit':
	    $this->Topic->id = $this->params['pass'][0];
	    $ownerId = $this->Topic->field('user_id');
	    if ($ownerId != $userId) $role = 'forum/mod';
	}
	return $this->Acl->check($aro, $role, '*');
    }

The code to add to PostsController is the same, replacing Topic with Post.

We’re pretty much done by now, only one thing is missing.

Assigning roles through the application

Now you can add users, but how do give the moderator or admin role to someone? You could use AclShell like we did before, but it would be nicer to be able to do it via web.

Granting access to roles with AclComponent

When a user shows to be trustworthy you can promote her to moderator:

// app/controllers/users_controller.php
    function promoteToModerator($userId) {
	$aro = array('model'=>'User', 'foreign_key'=>$userId);
	$this->Acl->allow($aro, 'forum/mod', '*');
	$this->Session->setFlash('User promoted to moderator');
	$this->redirect(array('action'=>'index'));
    }

If the user abuses her power and you have to demote her, call inherit instead of allow:

$this->Acl->inherit($aro, 'forum/mod', '*');

You could use deny instead of inherit but be aware of the difference. If at a later point you decide to grant the role to the user’s group/department (i.e. ARO parent node). With deny you actively block the user from the role, no matter what permissions her group has. With inherit you merely revert the access to the default values.

Now you can promote and demote users by accessing URLs directly. Building a complete control panel with role assignment is an exercise left to the reader :)

Well, that’s it for today. We have gone from not having any user information to having a sophisticated ACL controlling what users can do. As always, if you find any errors or have comments or criticism drop me a line.

A penny for your thoughts

(Your email is never shared.)