2012年2月17日 星期五

A lightweight approach to ACL - The 33 lines of Magic

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 ; ).

reference : http://debuggable.com/posts/a-lightweight-approach-to-acl-the-33-lines-of-magic:480f4dd6-639c-44f4-a62a-49a8cbdd56cb

沒有留言:

wibiya widget