Access Control Lists (ACLs) Part 3

In the first part, the idea and theory behind an ACL was discussed. In part 2, the set up of AROs, ACOs, and ACLs via the command line was shown. Now in part three, we look at why this is so important. Because an interactive site with memberships should never be static, what happens when a new member signs up? What happens when a member is promoted to an “admin” level? And what happens when users change? This can all be happened via ACLs.

In part 2, existing member were set up as AROs. And with user accounts, we also have to set those up as ACOs. Then those AROs (people) need to have permissions set for the CRUD actions. (Create, Read, Update, Delete). These actions are specific to the ACO, or object they are trying to manipulate. So if a user wants to edit their own account, do they have permission? If a user wants to delete another person’s account, do they have permissions to? With setting up ACLs, this can be checked. But what do we do when a new person signs up for an account? We need to create the code to do this.

In the Users Controller, we need to make sure we use the ACL component is included. So include this in the controller:

class UsersController extends AppController {
	var $name = 'Users';
	var $components = array('Acl');

Also remember that the Auth and Security components are also very powerful components and should be included as well, but the above only shows where to include the components. Now with this in place, we can no address the add (or register) function of the controller.


When a new user registers an account on the site, we want to make sure to give them only access to their own account, and be able to read the other user profiles. The first thing is to create an “add” (or register, I actually prefer that because it just seems to be more logical to me, but I will use the “add” function for this example).

function add() {

}

Now we need to create the basics, or even better, better Bake up the views for the User controller. This helps set the base actions needed to add an account. But here is what some of the included actions should be:

function add() {
	if (!empty($this->data)) {
		// Sanitize the stuff
		$clean = new Sanitize();
		$clean->clean($this->data);

		// Set the data to the model
		$this->User->set($this->data);

		// Clean all of the elements in the data array, Attendee, Organization, and Bill information areas
		$this->data['User']['username']	  = $clean->paranoid($this->data['User']['username'], array('.', '-', '\''));
		// Make sure you clean all the user input values to help clean the input from the user, this is just an example line

		// Set any defaults for the user table, these would be items that are not on the form, ie avatar default, last_login date or ip, just to make sure there is no XSS on those fields

		$this->User->create();
		
		if ($this->User->save($this->data)) {
			$this->Session->setFlash(__('The User has been saved', true));
			$this->redirect(array('action'=>'index'));
		} else {
			$this->Session->setFlash(__('The User could not be saved. Please, try again.', true));
		}		
	}
}

This is just some extensions of the Baked output from the Users controller. I always suggest to sanitze the data, and always set defaults if there are more fields in the table that do not have corresponding inputs. This just helps to cut down on XSS (cross site scripting) and helps to maintain some order of data expected to go in the tables. The next step is to add the ARO and ACO creations for this user.

Every user is an ARO, but they are also ACOs as they may have other users requesting to take action on their accounts. So we need to create the AROs and ACOs for the user right after the create() method:

. . . 
$this->User->create();
if ($this->User->save($this->data)) {
	$usr = $this->User->getLastInsertID();

First we need to get the last inserted ID, because this is the user’s new user_id. We need to use that here. Then we need to instantiate the ARO object

$aro = new Aro();

With the ARO object ready to use, we need to create some date to send to the ARO.

$user = array(
	'alias' => $this->data['User']['username'],
	'parent_id' => 4, // member ARO 
	'model' => 'User',
	'foreign_key' => $usr
);

We are creating the information we are to put in to the ARO, the alias (which is the username), the parent ID, in this case, the “Members” ARO is aro_id 4, (your parent_id may be different depending on how many AROs you have), the model, which is User in this example, and the foreign key which is the new user id that was just created. Now the Cookbook will say you do not need to put in the alias and the model/foreign_key. But I do because it makes the table easier to read, especially if you are looking at the table doing troubleshooting. It is easier for me, but feel free to find a good method for your own application.

Now take the data, create and ARO and save the data to it.

$aro->create();
$aro->save($user);

Creating the ACO is similar to the ARO, and this is how it would look.

$aco = new Aco();
$aco_usr = array(
	'alias' => $this->data['User']['username'],
	'parent_id' => 2, // User ACO id
	'model' => 'User',
	'foreign_key' => $usr
);

$aco->create();
$aco->save($aco_usr);

Now we have an ARO created, and an ACO, we need to make sure the ARO has certain permissions to the ACO (and its parent). Remember that a few rules for all new accounts is that they can only edit their own account, they can not delete any account, and they can view any account. So we can set these permissions after the ARO and ACO creations.

First we want to set it that they can only edit their own account. We are going to implicitly allow access to only this account.

$this->Acl->allow( array('model' => 'User', 'foreign_key' => $usr), $this->data['User']['username'], 'update' );

By setting the permission this way, it is only granting access to their own account in order to edit or update. If this ARO tries to update another account, it will not have the permissions needed to do so. Now we need to deny the delete action for all User ACOs.

$this->Acl->deny( array('model' => 'User', 'foreign_key' => $usr), 'Users', 'delete' );

This will deny all delete actions for this ARO. By specifying the “Users” ACO, we are safeguarding the delete action. This not only denies the delete action from happening on the “User” ACO itself, but all of its child nodes, or in other words, any ACO that has the “Users” ACO as it parent. Thus, this newly registered user can not delete any account in the system.

Here is it in its entirety:

function add() {
	if (!empty($this->data)) {
		// Sanitize the stuff
		$clean = new Sanitize();
		$clean->clean($this->data);

		// Set the data to the model
		$this->User->set($this->data);

		// Clean all of the elements in the data array, Attendee, Organization, and Bill information areas
		$this->data['User']['username']	  = $clean->paranoid($this->data['User']['username'], array('.', '-', '\''));
		// Make sure you clean all the user input values to help clean the input from the user, this is just an example line

		// Set any defaults for the user table, these would be items that are not on the form, ie avatar default, last_login date or ip, just to make sure there is no XSS on those fields

		$this->User->create();		
		if ($this->User->save($this->data)) {
			$usr = $this->User->getLastInsertID();
			
			$aro = new Aro();
			$user = array(
				'alias' => $this->data['User']['username'],
				'parent_id' => 4, // member ARO 
				'model' => 'User',
				'foreign_key' => $usr
			);
			
			$aro->create();
			$aro->save($user);
			
			/*****************************/
			$aco = new Aco();
			$aco_usr = array(
				'alias' => $this->data['User']['username'],
				'parent_id' => 2, // User ACO id
				'model' => 'User',
				'foreign_key' => $usr
			);
			
			$aco->create();
			$aco->save($aco_usr);
			
			// Set the permissions
			$this->Acl->allow( array('model' => 'User', 'foreign_key' => $usr), $this->data['User']['username'], 'update' );
			$this->Acl->deny( array('model' => 'User', 'foreign_key' => $usr), 'Users', 'delete' );
			
			$this->Session->setFlash(__('The User has been saved', true));
			$this->redirect(array('action'=>'index'));
		} else {
			$this->Session->setFlash(__('The User could not be saved. Please, try again.', true));
		}		
	}
}

So that will set a new ARO and ACO for a new user. You would follow the same path for an event, or group, but you would only set a new ACO, and the permissions as needed, based on requirements. But this is not the only thing to do. We still need to check these permissions. As it stands right now, any user can edit or delete any other user, because we have not included the ACL check yet. So to demonstrate this check, we will use the Edit function as an example.

In the edit function, there is an ID passed to the function. This is the ID of the account to be edited. We need to check if the currently logged in user has the permissions to edit this account. We do that by doing an ACL check.

$this->Acl->check(array('model' => 'model_name', 'foreign_key' => "id of logged in person" "alias of ACO", "action requested");

So for this example, it is the User ACO we are wanting, more specifically the user account requested to be edited, and the action is (of course) update. So the first thing is to get the alias of the user to be edited

function edit($id = null) {
	$info = $this->User->read(null, $id);
}

This will pull the id into an array called “info” and we are looking for $info[‘User’][‘username’]. Remember that your version may vary based upon the table info you have. Now we can do a check based on the currently logged in user, and in this example I am using the Auth component to get that.

if ( $this->Acl->check(array('model' => 'User', 'foreign_key' => $this->Auth->user('user_id')), $info['User']['username'], 'update') ) {
	// Do the edit stuff here
} else {
	$this->Session->setFlash(__('You are not allowed to edit this user.', true));
	$this->redirect(array('action'=>'index'));
}

This will check the currently logged in user to see if they have update permissions on the specified user_id’s ACO. If they do have permission, then it will go through the edit actions, checks, etc. If they do not, it bypasses that altogether and kicks them back to the index view with a message.

The same can be done for the delete action

function delete($id = null) {
	$info = $this->User->read(null, $id);
	
	if ( !$this->Acl->check(array('model' => 'User', 'foreign_key' => $this->Auth->user('user_id')), $info['User']['username'], 'delete') ) {
		// Do the delete stuff here
	} else {
		$this->Session->setFlash(__('You are not allowed to DELETE this user.', true));
		$this->redirect(array('action'=>'index'));
	}
}

Now this is just a simple example, and this same type of idea can be done for events, editing events, viewing events, etc. The same goes for groups and any other type of application where a permission check is needed.

Now, is my soap-box time. Why use ACLs at all? Wouldn’t a simple “level” table be enough to create this same type of effect? Yes and no. Remember that the right program for the job is determinant upon the job requirements. If an application is going to be used for a small purpose, and maybe you only will have 50 people at the most in a calendar application, then an ACL may be going a little overboard. However, if you have 50 people who are part of different groups, and based on group membership can do different things on the application, and the tasks may change from person to person based on work load, then an ACL may take some of the stress of of doing this type of thing.

Remember that an ACL is a great way to keep permissions in check with little human overhead involved. But sometimes the job requires that human overhead is needed, sometimes not. ACLs are great, and I use them frequently on big jobs. For the smaller jobs, I do not. But after this, hopefully you know better about what an ACL is, and how to use one in an application.

This Post Has 2 Comments

  1. Paul Gardner

    Stephen,

    I am having great problems trying to implement Auth and Acl with CakePHP and hoped this article may sort me out, but whilst I now better understand the creation of AROs and ACOs I still don’t know how to implement this in such a way that the system inhibits users from accessing certain sections of the site.

    I have created 3 groups (Master Admins, Admins, Users) and 3 users one linked to each group. I am assuming that if I deny the ‘Users’ group update rights for the ‘User’ ACO and a member of that group tries to update a user the system should stop them but it doesn’t.

    Any chance we can try and get a conversation going which leads to me successfully getting Auth and Acl working in CakePHP and we can also pad out this article with the results as I am sure it would get you a lot of traffic and kudos (there are a lot of people struggling with this issue).

  2. stephen

    Paul,

    I would be happy to help and get the solution put up here for everyone. Remember that Auth and ACL do different things. Using them together can provide a great security level. With the ‘User’ACO, make sure that the user is not part of another group.

    Make sure that you are checking the user ACO and not the group ACO. So what you will need to do, is check the userID who is trying to edit, the userID of the object being edited, and then make sure that they have permission.

    My order of check:
    1. grab the user account to be edited
    2. Do an ACL check in an IF statement against the logged in User’s ID to the account to be edited.
    3. If they are not allowed an ‘update’ action, then redirect them to a page with a message
    4. If they do have permission, then go forward with the logic.

    Also, make sure that the user account has an entry in the tables. The ARO table should include your 3 groups, and then have three entries for users with the parent_id being the corresponding group they belong to. The ACO should contain at least 1 parent group of objects which will be enacted upon. So in your example Users. And each user ought to have an entry in this table as well. The AROCS_ACOS table should have the entries for the users with the permissions to each ACO group (and user if need be).

    If this does not make sense, let’s keep going and maybe you could post where the issue is going wrong and we can resolve this. Or you can email me as well.
    http://www.hirdweb.com/contact/

Leave a Reply