Simplistic example of row-level access control with Auth, Security and App Model in CakePHP

Let me just preface this post, by saying that this is indeed a very much simplified example.
The main purpose here is not to provide a solution that will fit any application, but rather give a decent foundation to further expand your own solution.

The example is based on Auth (you should know a little about the Auth component), the Security component and a simple method in your App Model.

Let’s say we have some journaling app with Articles Controller, which has a “view” action.
We only want to allow access to view the journal articles to their owners, i.e. the users who’ve actually wrote the article (or journal entry).

To illustrate this further, I am going to attempt to access a URL such as:
http://www.example.com/articles/view/8

The goal is to ensure that article with ID = 8, actually belongs to me (AKA the currently logged in user), and if I start tampering with URL by changing the ID, the access to “view” should be denied (unless I happen to pick another article ID, which belongs to me).

In a typical setup we probably have an “articles” table with a “user_id” field, i.e. User hasMany Article.

With that in mind we can create an easy method in our App Model to check for user access:

function canAccess($userId = null, $primaryKey = null) {

        if($this->find('first', array('conditions' => array($this->alias.'.user_id' => $userId, $this->primaryKey => $primaryKey),
                                           'recursive' => -1))) {
            return true;
        }

        return false;
}

Update (05/13/2009): Some users pointed out that recursive = -1, might cause problems with following find() operations. I’ve updated the code to pass ‘recursive’ as part of the find(), which resets the value back to the original in the latest versions of cake. Either way, you don’t want to pull associated models on this simple check.
If you decide to reset recursive later on, or better yet, use Contanable behavior, this solution should work perfectly well.

The above function simply looks for a combination of Article.user_id and Article.id to ensure that the Article actually belongs to the logged-in user.

So how do we use this in the Articles controller?
Perhaps something like this:

function beforeFilter() {
   if (in_array($this->action, array('view'))) {
      if(!$this->Article->canAccess($this->Auth->user('id'), $this->params['pass'][0])) {
         $this->Security->blackHole($this);
      }
   }
}

If the action is “view”, we pass the logged-in user ID $this->Auth->user(‘id’) and the article ID $this->params[‘pass’][0] to check the access.
If no record with both ID’s is found, we “blackhole” the request, otherwise the access is allowed.

And there you have it, a very simple way to control row-level access to any action in your controller.
As mentioned, I’m hoping that this post will inspire some ideas and will let you expand further in your app.

P.S. Do not forget to include your Auth and Security components, and if you need to get some more info on both of them you can take a look at the CakePHP manual or even do a quick search on this blog.

m4s0n501
  • Javier

    Mmmm… very nice idea :-).

    I’ve got two comments about your canAccess function:

    1. Is it dangerous to set recursive to -1 and not setting it back?
    2. It could be interesting in some cases if it returned the result of the find operation. You wouldn’t have to change the code in your beforeFilter(), and you might want to assign the result directly so you don’t have to query the database again.

    Cheers.

  • http://www.ruicruz.com Rui Cruz

    @Javier, it’s no dangerous since after each query the recursive parameter is reset.

    What does happen with this metod is that you end up having more queries then necessary.

    You could just have one query that returns the information for the view and then assert if the user ID is authorized. a simple if would suffice.

    Something like this.

    if ($data[‘Article’][‘user_id’] != $this->Auth->user(‘id’))
    {
    $this->Security->blackHole($this);
    }

    If I’m wrong please correct me.

    • Javier

      @Rui Cruz, thanks for pointing out!

  • http://teknoid.wordpress.com teknoid

    @Rui Cruz, Javier

    As Rui Cruz pointed out, having recursive = -1, is not a problem.

    I think for this particular case comparing Article.user_id to the logged-in User.id is just fine, however for some reason I feel that extracting “unauthorized” data from the DB is just somehow wrong, perhaps this would be more applicable if I was retrieving a lot more data based on the given ID.

  • http://jmcneese.wordpress.com Joshua McNeese

    or, if you need more control, try RMAC: http://jmcneese.wordpress.com/2009/04/19/rmac-ftw-part-1/ :)

  • http://teknoid.wordpress.com teknoid

    @Joshua McNeese

    Thanks for sharing… Actually it was that very article and some additional debate that prompted me to post something overly simple for people to chew on ;)

  • http://jmcneese.wordpress.com Joshua McNeese

    ah, excellent. i’m glad that i was able to generate some buzz :)

  • Martin

    What is the “0” in “$this->params[‘pass’][0]” for? I understand that ‘pass’ stores the article ID.

    • http://teknoid.wordpress.com teknoid

      @Martin

      0 is index in the array of passed params, in other words the first passed param.
      You can find out more about the structure if you: pr($this->params).

      • Martin

        Thanks!

        But is it necessary? If the URL is view/ without a ID I get “Undefined offset: 0 [APP/app_controller.php..”. Is there a nice way to fix this?

  • http://teknoid.wordpress.com teknoid

    @Martin

    Yes, a simple isset() will do it…

  • Robert

    “however for some reason I feel that extracting “unauthorized” data from the DB is just somehow wrong, perhaps this would be more applicable if I was retrieving a lot more data based on the given ID.”

    You are right but in my opinion better solution would be to add condition to the where clause. I’m not sure how to do this in the app model, but it should be something like this:

    // if autorisation flag is not switched off and model has “user_id” field and (maybe) in the case field is not set:
    if($this->autorisation && exists($this->alias.’.user_id’) && null == $this->params[‘user_id’] ) {
    // add condition:
    $this->params[‘user_id’] = $this->Auth->user(‘id’);
    }

    This code obviously will not work, I have to check how to write it for cake…

    thanks,

  • http://teknoid.wordpress.com teknoid

    @Robert

    I’m sure there are other ways to safe-guard the data, but to me one quick query is unlikely to slow things down to the point of where it’s going to cause major problems.
    If it ever does, then it will be time to optimize…

  • http://www.petitbazarcarto.net Laurent Jégou

    For me the $this->recursive = -1; affectation caused problems , my controller actions did not reach the linked models.

  • http://teknoid.wordpress.com teknoid

    @Laurent Jégou

    If that’s the case, you may pass the ‘recursive’ => -1, in the array of options to find().
    Either way you don’t want any associated models to be pulled when doing a simple check, but thanks for pointing this out as it might’ve changed in the recent versions of the core.
    I’ve updated the code with a simple remedy.

  • http://www.petitbazarcarto.net Laurent Jégou

    Yep, this way it works, thanks.

  • http://teknoid.wordpress.com teknoid

    @Laurent Jégou

    No problem.

  • Frederick D.

    Great article! Thanks very much. I had this working for me, and even referenced your article on the Cake Google Groups to try and help some others. Now, it is not working for me and I am not able to figure out what changed.

    Would you be willing to help with this please? Thanks in advance.

  • http://teknoid.wordpress.com teknoid

    @Frederick D.

    Well, if you post some code I can certainly try.

    However, the best place to get help is on IRC (I’m often there) and google group.
    Can’t promise that I’ll answer here in a timely manner.

  • Frederick D.

    Just wanted to check first… thanks! Let me try here please.

    I’ve put into the app_model.php the tutorial code and it looks like this:

    function canAccess($userId = null, $primaryKey = null) {

    if($this->find(
    ‘first’,
    array(
    ‘conditions’ => array(
    $this->alias.’.user_id’ => $userId,
    $this->primaryKey => $primaryKey
    ),
    ‘recursive’ => -1
    )
    )
    )
    { // If record found where $userID and $primaryKey match, return true
    return true;
    }

    return false; // Otherwise, return false

    }

    In the distributors_controller.php, in the beforeFilter area, I have this:

    /* Execute this code before filtering */
    function beforeFilter() {
    /* Initialize Auth component first from parent */
    parent::beforeFilter();
    /* Check for mobile phone usage */
    if ($this->RequestHandler->isMobile() || strpos($_SERVER[‘HTTP_USER_AGENT’],’Android’)) {
    if ($this->RequestHandler->isAjax()) {
    $this->layout = ‘ajax';
    } else {
    $this->layout = ‘iphone';
    }
    $this->mobileView = ‘.iphone';
    }
    /* Check canAccess if the logged in user Id can access the requested user Id */
    switch($this->user[‘group_id’])
    {
    case ‘1’: /* Admin can access anybody */
    break;
    case ‘2’: /* Distributor can access only their distributors */
    if (in_array($this->action, array(‘view’)) or
    in_array($this->action, array(‘edit’)) or
    in_array($this->action, array(‘delete’)))
    {
    if(!$this->Distributor->canAccess(
    $this->Auth->user(‘id’),
    $this->params[‘pass’][0]
    )
    )
    {
    $this->Security->blackHole($this);
    }
    }
    break;
    default: /* Everything else is false */
    $this->flash(
    ‘You are not authorized to view that distributor.’,
    ‘/’,
    2
    );
    break;
    }
    }

    My core.php debug value is ‘2’. I did have the component ‘DebugKit.toolbar’ active in my app_controller.php. That was causing problems so I commented out that component. I was using a ‘flash’ message instead of ‘blackHole’. The ‘flash’ message was working in the past, but now it does not paint a new screen. Now the ‘flash’ message shows above my header and still retrieves and paints the record the user is not authorized to.

    I tried the ‘blackHole’ method and now receive a Cake error message of “Undefined property: DistributorsController::$Security” and “Fatal error: Call to a member function blackHole() on a non-object in /…/Sites/bgnation.ca/app/controllers/distributors_controller.php on line 40″.

    I have the ‘Security’ component active and working. I can log in as a distributor and access records successfully. When I try to simulate a request for an unauthorized record, the above error occurs.

    This must be something simple because it was working in the past. My other modifications to this controller were to switch from an HTML grid to a JSON object and ExtJS rendering of that data.

    Do you have any ideas what I am doing wrong? Thank you.

  • http://teknoid.wordpress.com teknoid

    @Frederick D.

    The error is very simple. It means that your Security component is not loaded.

    This can be due to a few reasons, for example… you override the components array somewhere or there is a spelling mistake. Unfortunately, I cannot give you an exact answer you’ll have to do some debugging.

    p.s. Please paste the code in the bin, because it’s impossible to read like this and takes up a little extra space ;)

  • Frederick D.

    Well sure enough. I have the ‘Auth’ component in the app_controller.php but not the ‘Security’ component. Adding the ‘Security’ component to the top of the list of components now does not allow me to log in at all with either debug at ‘0’ or ‘2’.

    I will have to check that out as now ‘Security’ is conflicting with what I have for ‘Auth’. That would explain why I cannot call the ‘blackHole’ without ‘Security’ enabled. Duh.

    Hopefully my use of JSON objects and ExtJS for forms and grids will not conflict with the use of the ‘Security’ component. We’ll see…

    Thanks!

  • http://www.bradmaxs.com Brad M

    Awesome article – couldn’t find anything easier that does the trick better and spent many hours trying!! You always have great info here!

    It is working great but one funny bug I can’t quite get. I used a redirect back to the users profile (for example) rather than a black hole if someone tries to view another profile which is not their own and I included a Session->SetFlash message. For some reason anytime I go to another link that also has a model the flash message is getting called even though the user is not viewing another profile.

    I looked at the pass array and it is not showing up anything but an empty array when I go to these other links and it is getting the correct pass offset when I am in profile.

    Any help is greatly appreciated.

    Thanks again.

  • teknoid

    @Brad M

    Very hard to guess without looking at the code… try to ask also on IRC or google group.