Central ACL Check

With checking ACL’s, the code I use is as follows:

$info = $this->Member->read(null, $id);

// Check for permissions to edit this account
if ( !$this->Acl->check(array('model' => 'Member', 'foreign_key' => $this->Auth->user('member_id')), $info['Member']['username'], 'update') ) {
	$this->Session->setFlash(__('You are not allowed to edit this user. -- ' . $this->Auth->user('member_id'), true));
	$this->redirect(array('action'=>'index'));
}

While this works and is not that bad of an idea at all, there is a way to centralize this check and put it in the main app_controller.php file.

Depending on the scope of the project, or the business needs of the application, this check can be done and centralized so that there is not the need to put the ACL check in its form above in every action in every controller that needs to have this done. So for the Member controller, you may need to copy/paste the code above to check the ACL for permissions to view, edit, delete, add, etc. That is just one controller, what if there were others as well, like Post, Event, Pictures, etc. That is going to be a lot of code to write to check the ACL.

Now even though the Cookbook on checking ACLs shows a simple one line approach:

$this->Acl->check(array('model' => 'User', 'foreign_key' => 2356), 'Weapons');

it is not just a simple one line approach. There are other things to consider, like what do you want to do if it fails. Is there a specific error message, does it need to redirect to a certain page, and what about if the person needs to be logged in and not need any special ACL permissions.This can get to be a few lines in length, and have a few more things that need to be done. This would be per function/action in the controller.

What I have done, is put the check in the app_controller. This way I need to check it with one line of code, instead of a few. I know, this may seem trivial, but this is something that could get really drawn out in heavier applications.

In the app_controller.php file, I created two functions:
_enforceAccess()
_checkAccess()

First I will explain the _enforceAccess() function. Since I use the model method of ACL, the function is looking for the following:
model
user’s ID
Object being acted on
Action for the object
Redirect Page

I also do a check for logged in status as well, to make sure that if an action is needed after login, like viewing, then I can check that here as well, instead of per function in the controller. I set all of the parameters to null, in case there is no needed permission check and I need to check logged in status.

So the way this function looks after everything is added:

function _enforceAccess($model = null, $id = null,$object = null, $action = null, $redirect = array("/")) {
	if (!$this->loggedin) {
		$this->Session->setFlash('You do not have permissions to carry out the requested action.');
    	$this->redirect('/members/login');
    	exit();
    }
	if ($this->_checkAccess($model,$id, $object, $action)) {
		return true;
	}
	$this->Session->setFlash('You do not have permissions to carry out the requested action.');
	$this->redirect($redirect);
	exit();
}

This function first checks for logged in status, if the person is logged in, then I go to the next step, if not, then I would redirect to the login page to get the user to log in to perform this action. As a side note to this: If you are using the Auth component, after the person logs in, the Auth component will redirect back to the prior page where this check was made.

After checking logged in status, it makes a call to the _checkAccess() function and passes the parameters needed to check the ACL. If everything works out ok, and the person has the permission to do the requested action on the object, then the code sends it back to the controller to do any other scripts. The person is authorized, and so it lets them through.

If the person does not have the permission, then they are redirected to the page that is set as they redirect. If no page is given, then the default is just the top level page, or home page of the application. If you specify a redirect page. Either way, the error message is printed out for the end user.

In the _checkAccess() function, it requires the parameters to check the ACL:
$model
$id
$object
$action
This function does the straightforward work.

function _checkAccess($model = null,$id = null, $object = null, $action = null) {
	// safety check just in case
	if ( !$this->loggedin ) {
		return false;
    }
    // Just checking for logged in status in this case
	if ( is_null($object) && is_null($action) ) {		
		return true;
	}
	// Check access to a specific record
	if ( !is_null($object) && !is_null($action) ) {		
		if ( $this->Acl->check(array('model' => $model, 'foreign_key' => $id), $object, $action) ) {
			return true;
		}
	}
	// Everything failed, so return false
	return false;
}

The first thing the function checks is a safety check. Just to make sure, we check to see if they are logged in. If they are not, return false. This is not really a necessary step, but for this application, one of the requirements was paranoid-like logged in checking. The next feature is to make sure logged in users are authorized. If there is no real check for permissions, and they just want to make sure the person is logged in, then this is one method I use to check this. Your application may be different, and that is ok. The final feature is to check a real object and action. We just put the ACL check here, and return true if it is allowed. If all of the IF statements do not get run, then it returns false. So when if you use this method, make sure that the needed checks are done, and the parameters that are passed have a real value and are not just NULL.

So this accounts for the checks I do now. I take out the calls in the controllers and replace them with this call. To see this in action, lets look at a few examples.

A member is logged in and would like to edit their friend’s profile:

function edit($id = null) {
    $info = $this->Member->read(null, $id);
    $this->_enforceAccess("Member", $this->Auth->user('member_id'),  $info['Member']['username'], 'update', array('controller' => "members"))
    // The rest of the action code goes here
}

This will do all the checks for you, and redirect to the members index page if the person does not have permission to edit other user’s account information.

Another example: a person who is logged in wants to view all posts. Posts are visible to only logged in accounts.

function view() {
    $this->_enforceAccess();
    // The rest of the action code goes here
}

Since this is open to everyone who is logged in, we do not need to send any parameters over, we just need to make sure they are logged in. Since the _enforceAccess() function already checks for this, we do not need to do any further checks in this function.

Another example: an admin wants to delete an account, and this account is the admin account, (yes this has happened before where an admin was “three sheets to the wind” and trying to perform an emergency delete of an employee who was terminated, and tried to delete their own account):

function delete($id = null) {
    $info = $this->Member->read(null, $id);
    $this->_enforceAccess("Member", $this->Auth->user('member_id'),  $info['Member']['username'], 'delete', array('controller' => "members"))
    // The rest of the action code goes here
}

Last example: A member is logged in and trying to view all events for the group they do not belong to:

function viewEvents($group_id = null) {
    $info = $this->Events->read(null, $group_id);
    $this->_enforceAccess("Event", $this->Auth->user('member_id'),  $info['Events']['group_id'], 'view', array('controller' => "events", 'action' => "comingup"))
    // The rest of the action code goes here
}

In this check, the code pulls in the groups events based on the foreign key of group_id in the Events table. We are enforcing the the access to view these events, based on the person’s ID. If the person’s ID does not have permission to view the events for that group, it will redirect the user to the page events/comingup. Which does it’s own little thing. Each example here will spit out the error message in the _enforceAccess() function.

Now, even though these two functions added some more lines of code, it has reduced what is needed to be done in each controller. Instead of doing a check in each function in each controller, with its accompanying lines of code, you have just one check to do, usually in one line of code.

What works so well for me in this, is that the _enforceAccess() method can be extended. If you want a generic error message, and then do special errors on certain cases, add a new parameter for the error message, and a check to see if it is filled out. If it is use the special error message, if not, then use the default. If you need additional permission checks, this method also provides a central point to do those.

Remember that in any application, it is all what is required by the business/client/needs. This may work in some instances, and not in others.

This Post Has One Comment

  1. Tim

    Great blog, good information.

Leave a Reply