Limit Data by User with CakePHP

CakePHPThrough the years, I have realized there are two types of web applications.

  • Community
  • Single User

While there are certainly some hybrid combination’s of these classifications, all applications can be categorized into these two categories. For example PHPNuke. It’s a single login to a community. As a user, you can use all of the functionality in the application AND you can see other users, etc. While something like FaceBook for example is a Hybrid of community and single user. While you can allow other users to see your data and content, they cannot modify your data or settings. You own that and nobody else has access to it without your username and password.

So comes the question. How can I limit data to a specific user with CakePHP? I love the CakePHP framework. But I have never been able to get a straight answer from anyone on the proper “cakeish” way to limit data to a specific user. For example. Let’s say I want to build a check book balancing application. I want it to be available to multiple subscribers. While all subscribers have access to the same functionality, they do not all see or modify the same data. While their data should be limited, they may all have access to the same Bank. This means that any user should be able to see all the banks we currently support… for example.

I have searched the internet and posted to stackoverflow.com trying to find the answer to this question. It is apparent that I am not the only one trying to figure this out. Add in the potential complexity to provide ADMIN routing and what you potentially have is a complicated mess of code if it is not done properly.

Well, search no more my Internet Friends! I think I have figured out the mess. I have been able to use a combination of things I have learned from here and here. But ultimately, I had to build this with good old ingenuity and a lot of trial and error. Keep Reading!

The Code

To start out with, there has to be a way to determine if the user is an ADMIN or just a standard USER. This will require that we put a ‘roll’ field in the users table. Here is what my table looks like.

--
-- Table structure for table `users`
--

CREATE TABLE `users` (
`id` char(36) NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(40) NOT NULL,
`first_name` varchar(35) NOT NULL,
`last_name` varchar(35) NOT NULL,
`role` enum('admin','user') NOT NULL DEFAULT 'user',
`active` tinyint(4) NOT NULL DEFAULT '0',
`created` datetime NOT NULL,
`modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

There is nothing really new about this table. It’s fairly straight forward and a typical users table. I use an email address for the username.

Now, the thing to keep in mind is I want to limit all of the data a USER sees to her own data while allowing any global data. This means there are a few checks that must occur each time the user accesses the site. Now for the “magic” piece that will accomplish this.

<?php
class AppController extends Controller {
beforeFilter() {
 if ($this->isAdmin()) {
     $this->beforeAdminFilter();
 } else {
     $this->beforeUserFilter();
 }
}

function beforeAdminFilter() {
 if ($this->Auth->user('role') !== 'admin') {
   $this->Session->setFlash(sprintf(__('%s is not authorized!', true), 'User'));
   $this->redirect('/users');
 }
}

 function beforeUserFilter() {
   Configure::write('user_id', $this->Session->read('Auth.User.id'));
 }

 function isAdmin() {
   return (isset($this->params['admin']) && $this->params['admin']) ? true : false;
 }
}

A few things I need to point out here. I use the StudioCanaria method for ACL. This is a great solution in my opinion. But I am not showing all of the code for that solution. It would make it more obscure and difficult to understand. This is just the vanilla portions I added in for my “limiting data” purposes.

Now for the more complicated part.

<?php
class AppModel extends Model {
 var $user_id;

function beforeFind($queryData) {
  if($this->user_id = Configure::read('user_id')){  // must be a user (not admin)
   if(isset($this->_schema['user_id'])) {
    if(isset($this->user_id)) {
     $queryData['conditions'][$this->name.'.user_id'] = $this->user_id;
    }
   }
   if($this->name == 'User') {
    $queryData['conditions'][$this->name.'.id'] = $this->user_id;
   }
  }
  return $queryData;
}

function beforeSave() {
 if($this->user_id = Configure::read('user_id')){    // must be a user (not admin)
  if(isset($this->_schema['user_id'])) {
   $this->data[$this->name]['user_id'] = $this->user_id;
  }
 }
 return true;
}

}
?>

Isn’t this how it is suppose to be? The heaviest part of the code should be in the models? Well this is the part that does the heavy lifting. Basically this will restrict anything that is associated to a USER. Ok, let’s dive in and I will explain how it works.

beforeFind

This is the function that is called before any search is done on the data. Now, keep in mind the two caveats to this configuration. 1- Not all data is associated to a USER and 2) ADMIN users are not limited. With that in mind, let’s analyze this code.

if($this->user_id = Configure::read('user_id')){  // must be a user (not admin)

This line establishes that we are in fact dealing with a USER. Notice I am using a “global variable”? This is so I can easily pass the data back and forth between the APP_CONTROLLER and MODEL_CONTROLLER.

Now comes the first part of the magic. Does this schema (Model) require a user_id field?

if(isset($this->_schema['user_id'])) {

This will determine if we need to actually “limit” the data. It will prevent the user from seeing anything accept the users data. How? Because if we detect a USER_ID, we set the condition to limit the data by the current user_id of the user looking at the data.

$queryData['conditions'][$this->name.'.user_id'] = $this->user_id;

The line before it is just because I am paranoid about my code breaking. So feel free to flame me about this:

if(isset($this->user_id)) {

But I will still leave it in. If you are comfortable taking it out, feel free. So there you have it. You are now limiting your $this->{MODEL}->find queries to a specific user. You will note that for the USER model, the user id is actually ID so I account for that in the next few lines. It is also important to note I use the standard foreign-key ID configuration common to good SQL practices. Any table associated to a user has the USER_ID as the foreign-key. I wont go into the horror stories of some of the things I have seen. (Including some of the work I have seen at Yahoo! while I have been consulting there.)

One more thing. If you have a beforeFilter in your model, it will override your app_model.beforeFilter. so you need to add the following:

<?php
class YourModel extends Model {
   // your code

  function beforeFilter(){
    parent::beforeFilter();  // <==== this is the important part
    // your filter code here
  }

  // your code
}
?>

Let’s continue.

beforeSave

This function is basically the same thing. However, as you may have guessed, it is to prevent a user from saving over another users data by chance they were able to randomly guess the ID.

Conclusion

There you have it! The only “user control” process for CakePHP that exists on the entire internet, at least that I am aware of. If you happen to know of another one that is more elegant, or you have a way to better improve this one, please drop a note in the comments. I find that many times I tend to overlook even some of the more simple solutions.

Happy Coding!

Like it? Post to your favorite location and share.
  • Digg
  • del.icio.us
  • Facebook
  • LinkedIn
  • Reddit
  • StumbleUpon
  • Twitter

4 Comments

Kim Pomares  on August 12th, 2010

Can this be used along with CakePHP’s ACL implementation as described in their ACL tutorial.

http://book.cakephp.org/view/1543/Simple-Acl-controlled-Application

Chuck Burgess  on August 12th, 2010

Yes. There should be no issue with it. ACL will control what actually has rights to CRUD (and custom functions) within each controller. This only manages the data that is queried and saved. It will limit it by the user. So if ACL does not allow them access, this functionality will not even be executed. I hope that makes sense.

jdrummey  on September 2nd, 2010

Thanks for writing this, I used the code almost exactly as written and it works well.

The one change I made is that I wanted admin users to get those rights from the admin group and not from a flag in the users table. Therefore, the login code checks if the user is in the admin group and sets an “admin_flag” Configure variable. Then that flag is looked for in the beforeAdminFilter() and before beforeUserFilter() functions.

Chuck Burgess  on September 2nd, 2010

That’s what makes this so flexible. That is also a great approach to the situation. Glad you liked it. I will soon be putting out my custom user management app that can be put into any cakephp app. I am still fine tuning the way it works. But it is fairly comprehensive. Keep an eye on the blog, I suspect it will be out in another few weeks.

Leave a Comment

You must be to post a comment.