Implementing ACL constraints, more than allow/deny
Asked Answered
E

2

8

I've developed a small yet effective MVC-style framework for use in an application, and I'm implementing an ACL per-request check.

Quick details: PHP 5.3+; MySQL 5.1+; Custom framework, "MVC-like"

As of now, the ACL check is simple "deny-if-not white-listing"; Each group can be assigned permission to certain request handlers. For example:

privilege                       permission
+----------+---------------+    +---------------+---------------+
| group_id | permission_id |    | permission_id | handler_key   |
+----------+---------------+    +---------------+---------------+
|     1    |       1       |    |       1       | lorem_ipsum   |
|     1    |       2       |    |       2       | hello_world   |
|     2    |       3       |    |       3       | foobar        |
+----------+---------------+    +---------------+---------------+

(user and group excluded for brevity, but their models are nothing unusual)

Anyways, my framework routes URIs to the appropriate handler_key via a handler/path table (to decouple the filesystem architecture) The request is then dispatched to the handler, given the group_id associated with the request is white-listed for that handler_key.

I'm curious, what is the best approach to implement storing/checking arbitrary (user-defined) constraints? Case examples would be:

  • Only permit the given group to invoke a handler weekdays, between 8:00 and 17:00.
  • Only permit the given group to invoke a handler to modify "owned" data; ie: data created by the associated user. This check would involve perhaps, a check of the user_id field associated with the content to be modified by the handler, and the user_id associated with the request

I had a flags column, however that is not future-proof with the introduction of more feature, group, and constraint requirements. I was thinking in the following direction, but what to use?

permission
+---------------+----------------------------+
| permission_id | handler_key   | constraint |
+---------------+---------------+------------+
|       1       | lorem_ipsum   |     ?      |
|       2       | hello_world   |     ?      |
|       3       | foobar        |     ?      |
+---------------+---------------+------------+

Unnecessary clarification:

(Note: code was typed here, not copypasta from project)

To clarify some of the jargon here; handlers (specifically web handlers) are essentially controllers, for those familiar with the MVC archetype.

Their specific implementation is a single PHP file, which returns a function to be called by the dispatcher, or sub-handler caller. For example:

<?php
    $meta = array('subhandlers' => array());
    return function($request, $response) use($meta){
        $response['foo'] = 'bar';
    };

My framework uses web handlers and API handlers; web handlers feed data into a response object (basically a collection of hierarchical views) which generates HTML. The data is obtained by invoking the API handlers, which simply return raw data (API handlers could be regarded as representations of the model, going back to typical MVC)

Compound handlers are essentially an abstraction layer, as they are handlers themselves that invoke handlers to aggregate data. My current implementation of the ACL check, does a cursory check of all nested handlers (via $meta, an array variable declared to act as the metadata header for the handler) For example:

<?php
    $meta = array('subhandlers' => array('my_subhandler'));
    return function($request, $response) use($meta){
        $someData = Caller::call('my_subhandler', array($request, $response));
        $response->bind($someData);
    };
Existent answered 25/5, 2011 at 22:9 Comment(3)
You may find this interesting: vimeo.com/2723800 (The ACL is Dead).Impolite
Also: en.wikipedia.org/wiki/Role-based_access_controlImpolite
Thanks Alix Axel; Interesting video, I skimmed it, I'll give it a full watch later; as for the wiki entry, I've read it previously and it got my brain percolating, however I'm looking for solutions. I'll all for learning how to fish, but right now, I just need to eat for a day.Existent
E
2

After having reviewed my architectural choices, it became apparent to me that I could take advantage of my handler style request-handling approach.

(Please, critique and/or ridicule and/or high-five this proposition as warrants)

A request to check if a user has permissions can, for these purposes, be seen as simply another request, therefore requiring it's own handler. For example:

permission
+---------------+--------------------------------------+- - - - - -+
| permission_id | handler_key | constraint_handler_key | arguments :
+---------------+-------------+------------------------+ - - - - - +
|       1       | lorem_ipsum | user_check_owner       |           :
|       2       | hello_world | user_check_owner       |           :
|       3       | foobar      | time_check             |           :
+---------------+-------------+------------------------+ - - - - - +

With the addition of a NULL-able constraint_handler_key column, new constraints can be programmatically created, and administratively delegated.

When a request fires, the framework aggregates the handler chain, and compares it against the white-list, as it already does. In the event a constraint_handler_key exists (possibly with arguments as the above table suggests) it performs a look-up in the handlerTable associated with that request type:

// web handlers handlerTable example
return array(
    'lorem_ipsum' => PATH_WEB_HANDLERS . 'path/to/lorem_ipsum.handler.php',
);

// acl handlers handlerTable, follows same format
return array(
    'time_check' => PATH_ACL_HANDLERS . 'time/check.handler.php',
);

(API handlers are also identical)

The ACL handler is expected to return only boolean values, thus continuing or ending the permissions check sequence, and therefore, the whole request.

// time_check acl handler, allow if current time between 9am - 5pm
return function($request){
    $dayBegin = time() - (time() % 86400);
    return (time() > ($dayBegin + 32400) && ($dayBegin + 61200) > time());
};

Cases of the omission of constraint_handler_keys are handled as basic white-list checks; allow-if/deny-if-not.

So my application directory will start looking like this:

:
+- application/
|   +- config/
|   +- views/
|   +- handlers/
|       +- acl/
|       +- api/
|       +- web/
:
Existent answered 26/5, 2011 at 6:32 Comment(1)
Hi - I'm interested to see if you progressed on this so you no longer have hard-coded terms like $dayBegin + 32400? As @Alix suggests, creating a mechanism to parse role-specific constraints from the database seems like a good way to go. I've been working on something similar, but yet to nail it.Deception
I
4

The video I posted in my comment has some similar ideas to the ones you shared, no code though.

Regarding your question, I've never implemented this and I have only a vague idea how such system could (or couldn't) be built, so take my input with a (or several) grain(s) of salt.

The first thing you must do is identify the constrains the user can restrict, forget about arbitrary data, defining what the user can and cannot define is the most important point here.

The next step is to define some sort of notation or data structure you can easily parse and validate, for instance, in time based restrictions you have the following possibilities:

  • specific datetimes (2011-05-26)
  • datetime ranges (2011-05-26 to 2011-05-31)
  • recursive datetimes (201x-05-26 or every friday)
  • logical (and / or / xor) operators

If you have a propper notation you can easily parse and validate this by tokenizing the rule and maybe using something DatePeriod or DateInterval or even the modulus operator like crontab does.

Your second example:

Only permit the given group to invoke a handler to modify "owned" data; ie: data created by the associated user.

Sounds like plain ACL to me:

  • resource: specific data
  • role: specific user

Then of course, you have the more complicated rules like:

The user who approves the record must have an equal or higher level (in the same security namespace) as the user who created the record, but they can't be the same user.

I think this kind of rules must be specific to the application logic and a true universal solution would be extremely difficult to implement, Zed Shaw mentioned in his talk that he came up with a complete solution that only uses 400 lines of code, I would be very interested in learning how he did that.

Flags (stored in bits) are a great (although cryptic) way of specifying a combination of one or several constrains but in my experience it's better if you define all the constrains apriori.

Since you don't seem to know exactly the constrains you want to check I suggest you create notations for the several type of restrictions you want to check:

  • time based
  • user/resource based
  • ...

And then implement methods that parse (maybe with a notation that implies the version, so you can migrate in the future) and validate all constrains.

I'm sorry if my answer doesn't help you much but I find the question very interesting and I hope someone else comes up with a better one, AFAIK there is no magic wand to do this.

Impolite answered 26/5, 2011 at 2:17 Comment(1)
Thanks Alix Axel; Your suggestions certainly got me thinking. Having reviewed my architectural choices, I think I have found a very fitting solution, which leaves implementation open ended and completely application agnostic. Check my own answer for updates, and please, feel free to critique.Existent
E
2

After having reviewed my architectural choices, it became apparent to me that I could take advantage of my handler style request-handling approach.

(Please, critique and/or ridicule and/or high-five this proposition as warrants)

A request to check if a user has permissions can, for these purposes, be seen as simply another request, therefore requiring it's own handler. For example:

permission
+---------------+--------------------------------------+- - - - - -+
| permission_id | handler_key | constraint_handler_key | arguments :
+---------------+-------------+------------------------+ - - - - - +
|       1       | lorem_ipsum | user_check_owner       |           :
|       2       | hello_world | user_check_owner       |           :
|       3       | foobar      | time_check             |           :
+---------------+-------------+------------------------+ - - - - - +

With the addition of a NULL-able constraint_handler_key column, new constraints can be programmatically created, and administratively delegated.

When a request fires, the framework aggregates the handler chain, and compares it against the white-list, as it already does. In the event a constraint_handler_key exists (possibly with arguments as the above table suggests) it performs a look-up in the handlerTable associated with that request type:

// web handlers handlerTable example
return array(
    'lorem_ipsum' => PATH_WEB_HANDLERS . 'path/to/lorem_ipsum.handler.php',
);

// acl handlers handlerTable, follows same format
return array(
    'time_check' => PATH_ACL_HANDLERS . 'time/check.handler.php',
);

(API handlers are also identical)

The ACL handler is expected to return only boolean values, thus continuing or ending the permissions check sequence, and therefore, the whole request.

// time_check acl handler, allow if current time between 9am - 5pm
return function($request){
    $dayBegin = time() - (time() % 86400);
    return (time() > ($dayBegin + 32400) && ($dayBegin + 61200) > time());
};

Cases of the omission of constraint_handler_keys are handled as basic white-list checks; allow-if/deny-if-not.

So my application directory will start looking like this:

:
+- application/
|   +- config/
|   +- views/
|   +- handlers/
|       +- acl/
|       +- api/
|       +- web/
:
Existent answered 26/5, 2011 at 6:32 Comment(1)
Hi - I'm interested to see if you progressed on this so you no longer have hard-coded terms like $dayBegin + 32400? As @Alix suggests, creating a mechanism to parse role-specific constraints from the database seems like a good way to go. I've been working on something similar, but yet to nail it.Deception

© 2022 - 2024 — McMap. All rights reserved.