April 24, 2009

Access Control List (ACL) with Code Igniter

In the last project I’ve worked on, I needed to install any kind of ACL to allow certain methods to be accessed only by some user roles, like website administration, etc. I’m building this website on Code Igniter, so I missed some related features that are available in other PHP frameworks like CakePHP or Zend.

After googling a bit, I found our several methods to get an ACL. Zend framework can be integrated into Code Igniter to use its library, but doesn’t seem really natural to me. I prefer having an previously constructed list, than building it ‘on the fly’ inside my code.

Then I fell into phpGACL, a free software project which allows to solve this problem on an easy way. It uses an API to connect to a database, where tuples of ‘controller’ – ‘method’ – ‘user’ can be stored (actually, AXO – ACO – ARO, acronyms of Access eXtended Object, Access Control Object, Access Request Object). And I developed my first version over this tool, on the most transparent way I was able to find:

  • There is a MY_Controller class where all other controllers inherit from
  • Constructor in this class launches a method, also in the parent controller, like $this->checkAccess(), so ACL is always evaluated before launching any other controller.
  • Method checkAccess gets user ID and role from session values (typically, in database) and calls phpGACL passing it three parameters: the user role, and current method and controller (both taken from the url string)

Note that I used the parent controller constructor to launch the user access control, but I could have used a hook. However, I preferred this for the sake of visibility: I’ll never forget what’s exactly running on every call to my controllers. But, this is only a matter of personal style.

The code would have looked like this:

class MY_Controller extends Controller
{
  var $gacl = NULL;
  function MY_Controller()
  {
    parent::Controller();

    // acl initialization
    include_once('path_to_phpgacl_library');
    $this->gacl = new gacl(array('db_host'     => 'acl_host',
                                 'db_user'     => 'acl_user',
                                 'db_password' => 'acl_pass',
                                 'db_name'     => 'acl_base',
                                 'db_type'     => 'acl_type' ) );
    // does the user have access?
    if (!$this->checkAccess())
    {
      redirect('','refresh');
    }
  }

  /**
     * Uses phpGACL library to determine user access.
     * Checks if a user has access to a method on a controller.
     * Takes user role from session.
     * Takes method and controller from $this->uri attribute.
     *
     * @return boolean if access is granted
     *
     */
  function checkAccess()
  {
    // variables
    $method     = $this->uri->rsegments[2];
    $controller = $this->uri->rsegments[1];
    $role       = $this->rdauth->getUserRole();
    //

    // I've created sections 'methods' for ACO's,
    //  'users' for ARO's and 'controllers' for AXO's
    return ( $this->gacl->acl_check( 'methods', $method,
                                     'users', $role,
                                     'controllers', $controller ) );
  }
}

Note also that this method runs automagically, converting url strings (controller and method) to phpGACL calls. Even though loading the default method for a controller (/index is omitted), internally CI auto fills this value as if it was written in url.

User role, as stated before, is returned by a custom library called rdauth, which will check session cookie to determine if there is a user logged in or not.

I used this system along several weeks, but finally I realized that – for my particular case – having all ACL definition in a database was not a good idea. Although phpGACL has an easy to use web interface to create these lists, and access control was separated from code, I found out these potential problems:

  1. Of course, ACL’s are out of code. That means that it database fails, or any careless user changes something there, all sites whose permissions depend on this database will allow the wrong user roles to access the undesired forbidden controller / methods. Not a good piece of news for paranoid administrators.
  2. Unless I create several databases, all historic branches, develop version and production version will share the same permissions. This is a bit messy to work with.
  3. When I’m coding I don’t have a clear idea of what methods can be accessed by the user role that I’m testing. If I develop a new method to test, I need to go to web admin tool to add it.

So there are benefits of using phpGACL, but also derived problems. Was at this point, when talking to a friend about this, that we realized that actually doesn’t matter if ACL’s are in databases or just arrays.

So the approach of the final solution implemented is:

  • Let’s change phpGACL with an access array in every controller
  • Let’s change checkAccess method to look into this array instead of calling phpGACL API

The main flow for the users access control remains unchanged, and it is still transparent from a coder’s view. Even we gain control over what’s happening with every controller, since $access array is very descriptive:

var $access = array('login'                  => array('visitors') ,
                    'resetPassword'          => array('visitors',
                                                      'registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'resetPasswordConfirm'   => array('visitors',
                                                      'registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'changePassword'         => array('registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') ,
                    'logout'                 => array('registered',
                                                      'editors',
                                                      'managers',
                                                      'administrators') );

Each method in the current controller has an entry in $access array. This entry is an array with all the roles that can load every method. Simple, isn’t it?

MY_Controller class then would look like:

class MY_Controller extends Controller
{
  var $gacl = NULL;
  function MY_Controller()
  {
    parent::Controller();

    // does the user have access?
    if (!$this->checkAccess())
    {
      redirect('','refresh');
    }
  }

  /**
     * Uses every controller access array to determine user permissions.
     * Checks if a user has access to a method on a controller.
     * Takes user role from session.
     * Takes method and controller from $this->uri attribute
     *
     * @return boolean if access is granted
     * @see phpGACL
     *
     */
    function checkAccess()
    {
        // variables
        $method = $this->uri->rsegments[2];
        $role    = $this->rdauth->getUserRole();
        //

        // check access
        if ($method)
        {
            if (array_key_exists($method, $this->access))
            {
                if (in_array($role, $this->access[$method]))
                {
                    return(true);
                }
            }
        }
        return (false);
    }

  }

So now, instead of just calling phpGACL API, I’m going over the access array, checking if there is defined an array of permissions for current method, and then if this array contains the current user role.

I haven’t finished defining users access control yet. There are further checks to forbid, for instance, avoiding that some user edited other users than itself, but these kind of ID checking will be implemented later (and perhaps explained on the following post).

What do you think? Do you use other approaches to get a good users access control list?