debuggable

 
Contact Us
 

A lightweight approach to ACL - The 33 lines of Magic

Posted on 3/10/06 by Felix Geisendörfer

Ok, I just finished a terrible (extended) weekend that featured 12 hours of CSS coding. The only reason I didn't loose my sanity was that I finally decided to figure out what the heck is wrong with IE. Those of you who have to do get their hands dirty in the field of graphics, css, and other non-php work from time to time as well, make sure to check out Position is Everything at some point, it really helped me out quite a bit so far.

Anyway, that's not really what I want to talk about today. One of the topics I have been very silent about for months is ACL. At the end of May I was somewhat unhappy with some of the things regarding the CakePHP DB ACL implementation. And it wasn't until last week that I finally decided to implement some basic rights management in one of my applications again. So since I didn't want to bother to frustrate myself with Cake's ACL again, I started to roll my own solution. While I was happily hacking away at some code, I suddenly realized that there were a lot of familarities between the code I was writing and the way ACL was working. A couple minutes later and I was already convinced that I basically had created 33 lines of code giving me pretty much all the flexibility CakePHP's well over 500 lines would ever give me.

But let me go a step back and explain my initial idea. My basic plan was to have a User belongsTo Group relationship and that each group entry in my database would have a field listing the controller actions the members of this group would be allowed to access. When helping another company to get their CakePHP based CMS done before the deadline I saw them using a Controller:action style syntax to do this which I liked. I modified it a little bit and came up with a Syntax like this:

Posts:index,Posts:view,Posts:admin_edit,Articles:index,...

But since I felt it was too much work to type in all Controller actions for the admin account I decided to create some wildchar functionality:

Posts:*,Articles:*

or even shorter:

*:*

But since I wanted the visitors of the page to be able to use any Controller action besides the ones starting with 'admin_', I had to add negative statements as well:

*:*,!*:admin_*

That's when I realized, wait, that's essentially the same thing as ACL. You start with some basic statement like DENY or ALLOW ALL at the beginning and then go down the tree (or the string) for the specific rules. All rules farther to the right in the ACL string will overwrite the ones farther left. So if you start out by saying Posts:* but add a !Posts:secret somewhere down the road, it means the group can access all Posts actions besides 'secret'. Or a little more creative set of rules could look like this:

*:*,!*:admin_*,*:admin_index

But since I wanted even more control, I decided to add an ACL string to my user table as well so I could make exceptions on a per-user basis, even if all users belong to the same group. The basic logic I used for that was to first check the access the User group had to a certain action, and then use this value as the default value for the user-specific check. That means if the group says yes and the user has no rule matching the current Controller:action, he's allowed to request it. But if he has a matching rule, this rule is used to determine the outcome regardless of the group's permission.

Ok, at this point I've got to disappoint you guys a little bit. I'm not quite ready to release my SimpleAuth / SimpleAcl class I'm using right now quite yet. The reason for this is that there is a very cool Security class coming with Cake 1.2 and I really want to make use of it as well. If you want the code anyway, I'll put it in Cake bin - it's fully documented and should be ready to go, but I won't be able to give you much suppport on it. But what I'll give you, are the 33 lines of Magic code I was talking about, the ones taking apart a given set of $rules in order to determine if an $object is allowed to access a certain $property:

function requestAllowed($object, $property, $rules, $default = false)
{
    // The default value to return if no rule matching $object/$property can be found
    $allowed = $default;
   
    // This Regex converts a string of rules like "objectA:actionA,objectB:actionB,..." into the array $matches.
    preg_match_all('/([^:,]+):([^,:]+)/is', $rules, $matches, PREG_SET_ORDER);
    foreach ($matches as $match)
    {
        list($rawMatch, $allowedObject, $allowedProperty) = $match;
       
        $allowedObject = str_replace('*', '.*', $allowedObject);
        $allowedProperty = str_replace('*', '.*', $allowedProperty);
       
        if (substr($allowedObject, 0, 1)=='!')
        {
            $allowedObject = substr($allowedObject, 1);
            $negativeCondition = true;
        }
        else
            $negativeCondition = false;
       
        if (preg_match('/^'.$allowedObject.'$/i', $object) &&
            preg_match('/^'.$allowedProperty.'$/i', $property))
        {
            if ($negativeCondition)
                $allowed = false;
            else
                $allowed = true;
        }
    }        
    return $allowed;
}

As you see, this is not specific to Controller actions. This can be used to control access on any kind of objects like Models, or other things. If you are familiar with CakePHP's ACL, you'll know there is nothing this does that CakePHP couldn't do. But what really makes me happy about this solution is the simplicity behind it. You don't have to really study ACL to grasp how it works, neither to you have to get into Modified Preorder Tree Traversal nor do you have to plan complicated Model-Aro-Aco relationships. You simply add a field called 'rules' to the Model (table) you want to control access on, and use the function to perform your security checks.

Some of you might point out performance issues, or the fact that the rights field shouldn't really be mixed in with the other Model fields. Heck, even all the rules should be seperate entries if you want to go for really high database normalization. But that's not what this solution is about, this solution is about simplicity. It's about being able to grasp the entire security concept in less then 5 minutes, avoiding all the dangerous complexity people usally tend to bring into this field. If you want to optimize, normalize or add more complexity in general, feel free to do so and let me know about the outcome ; ). But I think this is going for what most of us Baker's need in our daily kitchen work.

So, write a comment if you like this approach or if you see some issue with it, so I can make a fix before releasing the Auth/Acl bundle of 2 drop-in components at some point soon.

--Felix Geisendörfer aka the_undefined

PS: I got my car back this weekend, so the long promised cake party should be happening this week for sure! Which reminds me, Thursday is my birthday, so maybe I don't even have to buy the cake myself ; ).

 

You can skip to the end and add a comment.

scott lewis said on Oct 03, 2006:

Wow, just yesterday I wrote my own ACL implementation. Great minds think alike and fools seldom differ. :)

I went with the Cake infrastructure and built it around the Acl::check() functiionality. The main point of mine is that I used a JSON file to store the permissions. Which gives me the convenience of the file-based system, along with the (aro, aco, aco_action) tuple that Cake's Db ACL supports, but its INI ACL doesn't.

I'm planning on cleaning it up and publishing it next week, but if anybody's interested, here's the documentation blurb at the top of the acl.json.php file. Note the wildcards. ;)

# acl.json.php - JSON CakePHP ACL Configuration
# ---------------------------------------------------------------------

# Use this file to specify user permissions.

# aco = access control object (something in your application)

# aro = access request object (something requesting access)

#

# All lines starting with '#' will be ignored.

#

#

# ARO records are added as follows:

#

# 'uid': {

# 'groups': [ 'group1', 'group2', 'group3' ],

# 'allow': {

# 'aco1': '*',

# 'aco2': [ 'action1', 'action2', 'action3' ]

# },

# 'deny': {

# 'aco1': 'action4',

# 'aco3': '*'

# }

# }

#

#

# ARO Group records are added in a similar manner:

#

# 'group': {

# 'allow': {

# 'aco1': '*',

# 'aco2': [ 'action1', 'action2', 'action3' ]

# },

# 'deny': {

# 'aco1': [ 'action4', 'action5', 'action6' ],

# 'aco3': '*'

# }

# }

#

#

# The allow, deny, and groups sections are all optional.

# NOTE: groups names MUST NOT be the same as usernames.

#

# ACL permissions are checked in the following order:

# 1. Check for user denies (and DENY if specified)

# 2. Check for user allows (and ALLOW if specified)

# 3. Gather user's groups

# 4. Check group denies (and DENY if specified)

# 5. Check group allows (and ALLOW if specified)

# 6. If no aro, aco, or group information is found, DENY

Ben Hirsch said on Oct 03, 2006:

Funny, I've been reading this site regularly and realizing I agree with you on a lot of points and theories and it turns out we have the same birthday. Go figure.

Felix Geisendörfer said on Oct 03, 2006:

Ben: That's awesome, let's have a big party, where do you live? ; ).

Scott: Huh, JSON? Didn't know that was used for anything but AJA(X), but as long as it does the job ^^.

scott lewis said on Oct 04, 2006:

Well, JSON is a nice, clear format. I was originally going to use YAML, but while monkeying around with the layout of the file, I realized that all I needed were strings, arrays and dictionaries. So there was no point in adding a YAML parser to the projects when I could get those from JSON, a parser for which the project already had. :)

ben hirsch said on Oct 04, 2006:

Huntington, NY. Where are you?

Felix Geisendörfer said on Oct 04, 2006:

scott: I see.

Ben: Zwickau, Germany. Seems like one of us gotta get on a plane tomorrow ; ).

Nao  said on Oct 04, 2006:

I want the code anyway! :) Can you put it in Cake bin, plz?

Ben Hirsch said on Oct 04, 2006:

oh well.. :) I thought you were studying in the states... ha!

Felix Geisendörfer said on Oct 04, 2006:

Nao: Here you go: http://cakephp.org/pastes/show/b7c9f9545f91a858ef2fa859a05f1324 . But again, I can only give you limited support on this right now. If you don't understand how to implement I can help you, but other then that you'll be on your own ; ).

Ben: I wish I was ... But I'm still in German high school and I don't think I'll be able to afford studying in the states since it's a lot more expensive for foreigners for some reason. And college is pretty much for free in Germany, so it wouldn't make sense to take out a lone or something.

AD7six said on Oct 04, 2006:

Hi Felix,

About 3 weeks ago I started writing a blog on the topic of ACL and when I read your post I thought I should get around to finishing it off :). The approach mentioned here seems fine although administering it may be cumbersome - what happens if a user has more rules than will fit in the relavent field? It's never a good idea to try and store n results in a string field (IMO).

Anyway for contrast, for the past couple of months I have been getting down and dirty with the cake DB ACL solution and kicking back a couple of fixes (I hope they saved Nate a bit of time), the long and the short is that in doing so I converted my previous 'how to apply ACL to websites' thoughts into 'how to /easily/ use the cake db acl solution'. There seem to be changes afoot in v1.2 as there is a new class related to actions (https://trac.cakephp.org/browser/branches/1.2.x.x/cake/libs/controller/components/dbacl/models/acoaction.php) so it's possible that, as with much of my past efforts, it may require some rework or be obsolete some time in the future.

Anyway enough preambling the article is here: http://www.noswad.me.uk/MiBlog/ACLPart1

I'd welcome comments,

Cheers,

AD7six

Felix Geisendörfer said on Oct 04, 2006:

Ad7six: I agree, technically spoken it's not a 'good' idea to put n items in one string field. However, depending on what you are trying to do, it's very convenient! To me, this is a human-readable ACL format. MPTT is not. If you build a nice Admin interface where you list all Controllers / Actions (can be done using php's class inspection functions), and explain the user how to use wildchars, this can work out nicely. But if you got the time to build this big tower of logic & interface goodies aimed at providing rights management to users you wouldn't trust to spell the word 'security', then you'll probably need some more code ; ). This inspires me, I'll do a blog post on this topic right now ... *g*.

AD7six said on Oct 04, 2006:

>> If you build a nice Admin interface where you list all Controllers / Actions

That is exactly the topic of the next blog of mine on the topic - including a demo.

The code was based on ACM, but is almost completely rewritten to be easier to use and edit (if editing is required). Within the realm of what the cake db acl can do - it allows you to define anything and everything via the gui. Anyway - more on that when it's ready for public viewing.

Cheers,

AD7six

Brandon P  said on Oct 04, 2006:

As usual ... the genious inside of you emerges!

The methodolgy that I have adopted for naming methods within my controllers are based on the 3 "basic" types of users on a system.

1. Guests
2. Members

3. Admins

So my conventions for naming methods follow:

admin_* - Admin methods (admin_edit, admin_delete, etc)
member_* - Member methods (member_index, member_search, etc)

* - Guest methods (index, search, etc)

The 3 definition ...

Admin Group (*:*)
Member Group (*:*,!*:admin_*)

Guest Group (*:*,!*:admin_*,!*:member_*)

cover 99.9% of my permissions!!!

Also, with my hack (https://trac.cakephp.org/ticket/1143) the routes are setup easily:

$Route->connect ('/account/:controller/:action/*', array ('prepend_action'=>'member_'));
$Route->connect ('/admin/:controller/:action/*', array ('prepend_action'=>'admin_'));

Great work! GREAT work!

Philip  said on Oct 04, 2006:

Thanks Felix, this looks understandable, even to me. I wonder though, if you could but your most logical brain power to the one small piece of the acl jigsaw that always seems to be ommitted - that is data access control (rather than function access control). i.e. a generic way of giving (say) an admin access to administrate a subset of data?

I have struggled to design anything that implements both aspects of acl in a simple manner...

Felix Geisendörfer said on Oct 05, 2006:

Brandon: I'm glad to see this work out so nicely for you ; ). But, instead of hacking the dispatcher I would recommend to manipulate the $_GET['url'] string in bootstrap.php. Or do you have a specific reason for not doing that?

Philip: Well let's say you want to control rights on every individual Post of your posts table. You could do that by simply adding a field called user_rights to the table and then filling it with user_id:action rules. When retrieving a Post from the table you could use the requestAllowed function from above to check if let's say User 5 is allowed to perform 'edit' on the Post or not. Or do you mean something different?

Philip  said on Oct 29, 2006:

Sorry for the tardy response - I've been away. I see where you're heading, but I like the idea of seperating acl from the data model.

If we stick with a psuedo Blog/CSM model, you'll see how quickly the acl 'mess' can grow once you try to partition the dataset. In our hypothetical CMS, it is divided into Sections. Each Section is transparent to a reader, but autonomous on the admin side. i.e. all categories, tags, posts etc are 'owned' by a section administrator and their contributors. This means the admin of one section cannot create, update or delete the entities 'owned' by other logical sections of the CMS. They may be able to assign other section's categories to their posts if the category owning section id a parent (I forgot to say that sections can be hierarchical :) The same applies to tags, posts, uploads, downloads etc.

I have come across this type of acl scenario so many times and each time I end up with a bit of a furball implementation... usually more on the maintenance side of acl than the 'run time' side.

I'm sure there is a generic way to approach this, but I've yet to work it out. In my head I see it as another 'layer' beneath the standard acl model (like yours) where when you you assign the group to an action you also specify a data set ID (in this example a section ID) but that doesn't automatically take care of the section hierarchy :)

Philip

P.S. Congrats on making it into the Cake contrib team.

[...] So if up to know you've always included all JS for every page load, this might help to reduce bandwidth usage for both you, and the users of your application. I know this is not the most advanced solution one could come up with. I thought about using my light weight ACL algorythm for the problem. But then I decided that it's too simple a problem for that kind of bloat. [...]

Felix Geisendörfer said on Jan 31, 2007:

A kind soul has just informed me that the Cake bin link to my SimpleAcl/SimpleAuth component above has expired, so I reposted the code:

SimpleAclComponent: http://bin.cakephp.org/view/461496756
SimpleAuthComponent: http://bin.cakephp.org/view/1099483994

Dieter@be said on Apr 12, 2007:

This is something really nice :-)

Btw instead of substr($allowedObject, 0, 1) you could do $allowedObject{0}.
This method is even explained on php's substr page.

Keep on baking!

Felix Geisendörfer said on Apr 13, 2007:

Dieter: Nice trick. It's amazing how I still learn stuff about PHP after all this years every once in a while ; ). Thanks a lot.

TM  said on Apr 25, 2007:

Great component. A real life saver. For any other rookies like myself having trouble putting it to use make sure you have have the following:

Groups table - must have a column named 'controller_acl' (rules go here)
Users table - must have a column named 'controller_acl'

You must include the User and Group models in each controller to be restricted

class CommentsController extends AppController {
var $name = 'Comments';

var $uses = array('Comment','User','Group');

var $components = array('SimpleAuth','SimpleAcl');

You'll need to create login/logout functions in a controller of your choice (User). The basic example in the Cake manual works for a start.
http://manual.cakephp.org/appendix/simple_user_auth

azzis  said on Apr 26, 2007:

Can you write an example how to use your components?

Felix Geisendörfer said on Apr 26, 2007:

Azzis: Not right now, sry : /. I'll make a post if I find some time to do so.

azzis  said on Apr 26, 2007:

Don't warry... i see your code and understand how component working.

jp09  said on May 29, 2007:

Is there a sample code out? Still a newbie but I'm really interested in implementing ACL on my project site.

Felix Geisendörfer said on May 29, 2007:

jp09:

Yeah, but it's experimental:

SimpleAclComponent: http://bin.cakephp.org/view/461496756
SimpleAuthComponent: http://bin.cakephp.org/view/1099483994

However, addons.mozillla.org seems to be using the code successfully so it can't be all that bad : P.

jp09  said on May 30, 2007:

Thanks Felix.

Is there an example out there that uses your components? It would be great if there is. =)

Felix Geisendörfer said on May 30, 2007:

jp09: http://svn.mozilla.org/addons/trunk/site/app/

jp09  said on Jun 08, 2007:

Thanks Felix.

I've only been able to get the code. Which files should I pay attention to if I just want to use ACL only? Sorry. Real noob here. =)

Felix Geisendörfer said on Jun 08, 2007:

jp09: /app/components/simple_acl.php + /app/components/simple_auth.php + / app/app_controller.php.

Dieter_be said on Jun 09, 2007:

Another slight improvement for your code Felix:

You can replace this:
if ($negativeCondition) $allowed = false;

else $allowed = true;

with this: $allowed = !$negativeCondition;

PS: for those who do not yet know, I used Felix' access control logic for my masters thesis.
On this page you can download the full report. It's in dutch however, but maybe it proves useful for somebody.

benedict  said on Jun 12, 2007:

Hi! Is there a simple example using simple_auth and simple_acl? I'm having a hard time with the one on http://svn.mozilla.org/addons/trunk/site/app/

Thanks!

Felix Geisendörfer said on Jun 12, 2007:

benedict: Not at this point - no. Sorry : /.

jp09  said on Jun 13, 2007:

One more thing, what sample data can the users, groups and groups_users contain?

Felix Geisendörfer said on Jun 13, 2007:

jp09: Not sure if I understand your question.

jp09  said on Jun 13, 2007:

Felix: The rules should be on the groups table right? What will the groups_users table contain? Also on the users table, there won't be any data that the components need(aside from the id)?

Felix Geisendörfer said on Jun 17, 2007:

jp09: You are free to set this up just like you need it. The method represented here is a generic way to define a list of object:property access rules and it's up to you to determine what they refer to and where to store them. So put them wherever it makes most sense to you ; ).

Digital Hazard said on Jul 01, 2007:

CakePHP | Google Groups...

Trước kia, tôi thường chỉ biết đến việc trao đổi, thảo luận trong forum mà ít khi ngó ngàng đến Google groups hay Yahoo! groups. Phải đến khi bắt tay vào tìm hiểu CakePHP, tôi mới nhận ra: Google Groups là...

einstein  said on Mar 25, 2008:

code igniter, try it is much better

Felix Geisendörfer said on Mar 29, 2008:

einstein: I especially like the "PR" they are using to prove themselves ; ).

Christopher Vrooman  said on Jun 30, 2008:

Hello,
First of all, thanks for publishing such a slick pair of components. I got them working, except for one small bump. Each time I login for the first time, I get the following error in simple_auth.php:

Fatal error: Call to a member function findById() on a non-object

Which is related to the line:

$user = $this->Controller->{$this->userModel}->findById($id);

After I reload the page, all is well and I get where I want to go and as I navigate the the error does not appear again; however, with debug = 0; all I get is a blank page which is obviously undesirable.

I tried var_dump($this->Controller->{$this->userModel}) and it presents me with what looks like a valid User controller object. But obviously PHP isn't buying it.

Any suggestions?

Thanks in advance,
Christopher.

Christopher Vrooman  said on Jun 30, 2008:

Whoops,

I forgot to add that I'm using Cake v1.2.0.7296-rc2
-Christopher.

Christopher Vrooman  said on Jun 30, 2008:

Looks like I found the problem...

I had to set

$this->Auth->autoRedirect = false;

in my app_controller.php beforeFilter() function to enable me to do a

$this->SimpleAuth->setActiveUser($this->Auth->user('id'), true);

before redirecting to my Admin start page.

And it wouldn't hurt to put a:

$this->SimpleAuth->setActiveUser(null, true);

in the logout function to reset the activeUser to it's default.

Another thing somebody might find useful is this:
I was unable to access SimpleAcl from a template file (unlike the mozilla addon site using v1.1 where they just used $this->controller->SimpleAcl->setActiveUser(...), so I did the following:

in the beforeRender() function of app_controller.php, I have:

if ($this->Auth->user())
{

$this->set('simpleAcl', $this->SimpleAcl);

}

So if the user is logged in, then the $simpleAcl variable will be available in any administrative views, elements, etc that need to do something like:

< ?php
if ($this->simpleAcl->actionAllowed('Superuser', '*', $session->read('User'))):
?>

show Superuser controller related

content here for all actions

< ?php
endif;
?>

Cheers,
Christopher.

Christopher Vrooman  said on Jun 30, 2008:

Last whoops!

if ($this->simpleAcl->actionAllowed('Superuser', '*', $session->read('User'))):

Should be:

if ($simpleAcl->actionAllowed('Superuser', '*', $session->read('User'))):

without the $this->

Note to self, get more sleep.
- Christopher.

Felix Geisendörfer said on Jul 01, 2008:

Christopher: Please check the release date on those components. I actually just use the function as shown in this post without any components these days. I should do another post about it.

Éber said on Aug 14, 2008:

You definitely should! I would love...
Working with ACL on SQL Server is really a pain... This method would make things really easier...

Oliver Treend said on Jan 11, 2009:

Have you made the finished version with Cake 1.2?
I'd really love to start using it in my recipes, but I wanted to wait until you bring out the final Cake 1.2 version..

Thanks!

This post is too old. We do not allow comments here anymore in order to fight spam. If you have real feedback or questions for the post, please contact us.