How to limit user access in CakePHP: findMy

This article is also available in the following languages:
By harisenbon
Perhaps like many people starting a CakePHP project, I created a site where users could log in and create/modify their own files (in my case Japanese flashcards and tests) while not being able to screw with other people's stuff.

One would think that you could solve this with some nifty ACL and Auth work, but if you thought that, then you would be wrong.
Unforunately ACL only lets you determine what actions a user is allowed to perform, not on which items they're allowed to perform it.

And Auth only checks to make sure a person is logged in, not who they are or what they're doing.

The Traditional Way

However, thanks to the glory of cake, it's not that hard to limit a search by user! Just replace your find with the following function.

Controller Class:

<?php 
$my_file 
$this->SomeModel->findAllByUserId($this>Auth->user(id);
?>

And we're done!

Oops! Not so fast!

We just got all of the SomeModels! Well crap, I guess we'll have to make an option array

Controller Class:

<?php 
$opt 
= array(
   
'conditions' => array(
      
'SomeModel.user_id' => $this->Auth->user('id'),
   ),
   
'order' => 'SomeModel.date DESC',
);
$my_file $this->SomeModel->find('first'$opt);
?>

That's a lot of work for just getting my latest SomeModel. Even more work if I have to replace every instance of a simple find with something like that. I also need to do the same thing for every save and delete as well to make sure that they're not saving over someone else's data.

Sure, I could put that code throughout all my Controllers, but that's messy -- and if I forget to put it somewhere someone gets their data deleted.

So what can we do?

Well, I don't know about you, but I put a function in my AppModel file that lets me easily make sure that whoever's touching the file has permission to do so.

Model Class:

<?php 
function findMy($type$options=array())
{
   if(
$this->hasField('user_id') && !empty($_SESSION['Auth']['User']['id'])){
      
$options['conditions'][$this->alias.'.user_id'] = $_SESSION['Auth']['User']['id'];
      return 
parent::find($type$options);
   }
   else{
      return 
parent::find($type$options);
   }
}
?>

What this does

  1. Make sure that the model has an 'user_id' field
  2. Make sure that the user is logged in
  3. Add the user_id condition to the find options
    • Be sure to use the $this->alias in case you're using an alias for the Model in a BelongsTo or HasMany relationship.
  4. Find the data
This is a pretty simple function, and can form the base for any logic that you want to do.

For example, if you want to allow Admins to view anything, just add an $_SESSION['Auth']['User']['role'] == 'admin' to the mix.

If you want to allow users access to any 'public' items, add a condition for 'OR is_public == true'

Extending the function

This function is set in the AppModel, so it can be overridden in any of your defined models if you need to do any custom filtering on a per-model basis.

Also, it is possible to extend this to save and delete functions, to make sure that users are only saving or deleting their own files. If they try to delete a file that doesn't belong to them, return false and alert the user that that file could not be found.

Model Class:

<?php 
function deleteMy($id null$cascade true)
{
   if (!empty(
$id)) {
      
$this->id $id;
   }
   
$id $this->id;

   if(
$this->hasField('user_id') && !empty($_SESSION['Auth']['User']['id'])){
      
$opt = array(
         
'conditions' => array(
            
$this->alias.'.user_id' => $_SESSION['Auth']['User']['id'],
            
$this->alias.'.id' => $id,
            ),
         );
      if(
$this->find('count'$opt) > 0){
         return 
parent::delete($id$cascade);
      }
      else{
         return 
false;
      }

   }
   else
      return 
parent::delete($id$cascade);
}
?>

Conclusion

The power of CakePHP comes from it's infinite extensibility, and the fact that at it's core, it's still just a PHP program.

While I recommend following MVC practices, and to use the built-in CakePHP functions as much as possible, there are times when you just need to do it the simple way.

Also, for those who balk at my use of $_SESSION, I talked with one of the CakePHP core developers at a conference a while ago, and was asking him about this problem.

I asked,
"Why is Auth only available in controllers? It would be much more useful if we could use it everywhere. Is it a design decision?"
He replied.
"Because it's a component. That's the only reason."
Remember: the framework is there to help you. You are not its slave.

Taken from:
http://blog.japanesetesting.com/2010/05/07/how-to-limit-user-access-in-cakephp-findmy/

Comments

  • Posted 02/25/11 06:01:14 AM
    This is great and I got working but I cant seem to figure out how to limit edit / save... If a user goes directly to the edit page with the id of someone's data ( http://host/edit/23) they can edit it.
  • Posted 09/28/10 05:56:35 AM
    I am new to this stuff but couldn't you use hasmany?
  • Posted 09/04/10 12:07:04 PM
    How would this work with pagination?
    • Posted 09/04/10 04:02:46 PM
      How would this work with pagination?

      Controller Class:

      <?php function index() {
          
      $this->paginate array_merge($this->paginate$this->Post->findMy('all', array('paginate' => true)));
          
      $this->set('posts'$this->paginate());
      }
      ?>
  • Posted 08/30/10 06:16:59 PM
    Could (or should) this be a behavior?
  • Posted 08/28/10 09:13:21 PM
    I agree with Nick Baker, the functions should definitely return false if the conditions aren't met. This is a nice approach though, and helps cut down on redundant code in the controller. I recently saw a suggestion to put logic similar to this in the beforeFind, which is just asking for trouble in my opinion.

    I wanted to use this idea with multiple models, and be able to change which field would be checked with each model. You could even change the field to check from an action in the controller. I'm using the Configure class to set the current User and Site from the AppController, like so:

    Configure::write('User.Current', $this->Session->read('Auth'));
    Using the Configure class probably isn't any better than using $_SESSION, and if you have a better way, please let me know. In this case, using the Configure class allows me to pass non-Auth data to my models.

    Now for the AppModel, this is just an example and is fairly specific to my app, but here goes:

    Model Class:

    <?php 
    class AppModel extends Model {

        
    // default field to check when using the findMy and deleteMy functions
        
    var $fieldToCheck 'site_id';

        function 
    getFieldToCheck($field null) {
            if (empty(
    $field)) {
                
    $field $this->fieldToCheck;
            }
            if (!
    $this->hasField($field)) {
                return 
    false;
            }
            switch (
    $field) {
                case 
    'site_id':
                    
    $data Configure::read('Site.Current');
                    if (isset(
    $data['Site'])) {
                        
    $data $data['Site'];
                    }
                    break;
                case 
    'user_id':
                    
    $data Configure::read('User.Current');
                    if (isset(
    $data['User'])) {
                        
    $data $data['User'];
                    }
                    break;                
            }
            if (isset(
    $data['id'])) {
                return 
    $data;
            }
            return 
    false;
        }

        function 
    findMy($type$options = array()) {
            if (
    $check $this->getFieldToCheck($this->fieldToCheck)) {
                
    $options['conditions'][$this->alias '.' $this->fieldToCheck] = $check['id'];
                return 
    parent::find($type$options);        
            }
            return 
    false;
        }
        
        function 
    deleteMy($id null$cascade true) {
            if (!empty(
    $id)) {
                
    $this->id $id;
            }
            
    $id $this->id;
        
            if (
    $check $this->getFieldToCheck($this->fieldToCheck)) {
                
    $options = array(
                    
    'conditions' => array(
                        
    $this->alias '.' $this->fieldToCheck => $check['id'],
                        
    $this->alias '.id' => $id,
                    ),
                );
                if (
    $this->find('count'$options) > 0) {
                    return 
    parent::delete($id$cascade);
                }            
            }
            return 
    false;   
        }
    }
    ?>

    So for example in my Profiles model, or my Sites model, I can set $fieldToCheck to 'user_id', and that field is checked rather than the default site_id.
  • Posted 08/23/10 09:51:24 PM
    Nice article, I like the idea you're going for, but you have an issue with your findMy and deleteMy that is rather glaring.

    All anyone would have to do to bypass your ___My security is be logged out, because the default behavior (find/delete) is executed if the Session.Auth.User.id empty. A logged out user could still delete any other user's records by just navigating to their respective delete action with any id.

    If you're opting to use ___My instead of just the default find/delete you should require the existence of the user_id field along with the session, otherwise false should be returned. This is because you're explicitly making a choice to use findMy/deleteMy instead of just find/delete.

    It should really be this:

    Model Class:

    <?php 
    function deleteMy($id null$cascade true){
       if (!empty(
    $id)) {
          
    $this->id $id;
       }
       
    $id $this->id;

       if(
    $this->hasField('user_id') && !empty($_SESSION['Auth']['User']['id'])){
          
    $opt = array(
             
    'conditions' => array(
                
    $this->alias.'.user_id' => $_SESSION['Auth']['User']['id'],
                
    $this->alias.'.id' => $id,
                ),
             );
          if(
    $this->find('count'$opt) > 0){
             return 
    parent::delete($id$cascade);
          }
       }
       return 
    false
    }
    ?>
    • Posted 11/05/10 10:39:29 AM
      [quote] Nice article, I like the idea you're going for, but you have an issue with your findMy and deleteMy that is rather glaring.

      All anyone would have to do to bypass your ___My security is be logged out, because the default behavior (find/delete) is executed if the Session.Auth.User.id empty. A logged out user could still delete any other user's records by just navigating to their respective delete action with any id.

      If you're opting to use ___My instead of just the default find/delete you should require the existence of the user_id field along with the session, otherwise false should be returned. This is because you're explicitly making a choice to use findMy/deleteMy instead of just find/delete.

      It should really be this:

      Model Class:

      <?php 
      function deleteMy($id null$cascade true){
         if (!empty(
      $id)) {
            
      $this->id $id;
         }
         
      $id $this->id;

         if(
      $this->hasField('user_id') && !empty($_SESSION['Auth']['User']['id'])){
            
      $opt = array(
               
      'conditions' => array(
                  
      $this->alias.'.user_id' => $_SESSION['Auth']['User']['id'],
                  
      $this->alias.'.id' => $id,
                  ),
               );
            if(
      $this->find('count'$opt) > 0){
               return 
      parent::delete($id$cascade);
            }
         }
         return 
      false
      }
      ?>
      [end quote]
      I doubt that what you say is true. When you use this function you also use the Auth component en probably block delete actions for non-logged users?

Comments are closed for articles over a year old