I'm familiar with a whole bunch of ways to authenticate users for the web-based administration application we're developing, and even various techniques to keep tabs on authorisation...
However, my question to you is, how would you recommend I implement a fine-grained access control mechanism that offers the following:
- Users belong to 'roles' or 'groups' such as 'salesperson', 'planning', etc.
- The admin menu system shows only 'pages' which have functions relevant to the users role(s)
- Specfic functions within those pages have constraints - for example, on the 'new booking' page, 'salesperson' users can issue a booking 'only in the future', and on the 'edit bookings' page can edit bookings 'one week from now'. However, 'planning' users might be allowed to retrospectively book 'up to one week ago' and edit bookings made by themselves for 'any time period', but bookings made by others only 'up until tomorrow'...
I know I can implement a basic role-based system to satisfy no.1... I have a feeling I should split the entire application into code chunks, each with their own objectID-permissionID relationship so that I can scan the database of permissions to see which objects are available - that would help me with no.2.
Any ideas how I might build the form control for example, which for 'sales' users only displays a date in the future (but displays dates up to 'one week ago' for planning users), then somehow pairing that with a line in the POST parser that checks to see if the date is in fact within the expected range?
I've played around with the idea I should save each code chunk to the database, then have an object table which dynamically builds the code according to the permissions table, so that the only 'file' on the server is the db connection file!
Any ideas welcome... (even if your background isn't php/MySQL)
Some more insight into the problem from a CUSEC presentation by Zed Shaw talking about why "the ACL is dead" - http://vimeo.com/2723800
Warning, a lot of Zend Framework ahead!
You can easily handle 1. and 2. with Zend_Acl and Zend_Navigation .
For number 3 you will have to query the ACL object in your model and do a lot of stuff by hand. You could utilize Zend Framework for the forms as well and include specific form element validators depending on the users role permission.
EDIT:
If you do not feel like going the ZF route you can at least take a look at how the ACL is handled in ZF.
If you want to build real fine-grained access control (FGAC), just check my article on this subject for MySQL :
MySQL 5.0 Fine-Grained Access Control (FGAC)
Basically, you don't want your business code be dependent on the FGAC implementation, you don't want to mix FGAC code in where
clause of the select
statements of your business rules.
This article shows solutions to avoid cluttering of SQL statements.
In a bid to implememt a 'native' approach, rather than piggy-backing a framework, I've been playing around with the following. Would anyone rate this approach? Do you foresee any pitfalls?
// Check database for existence of this $user against this $object.
function get_permission($user, $object){
// Query goes here...
if( ... ){
return $permission;
} else {
return FALSE;
}
}
The above function would query the database and output something like this:
// Result of role-object query.
role_ID object_ID permission
------- --------- ----------
salesperson new_booking_date 'min' => 'now', 'max' => '+1 year'
planning new_booking_date 'min' => '-1 week', 'max' => '+1 year'
salesperson edit_booking_date 'this_user_min' => 'now', 'this_user_max' => '+1 week', 'other_user_min' => 'now', 'other_user_max' => '+1 week'
planning edit_booking_date 'this_user_min' => '-1 week', 'this_user_max' => '+1 year', 'other_user_min' => '-1 week', 'other_user_max' => '+1 week'
The following code in the page containing the form input:
// Draw form control with javascript date validation...
$this_permission = get_permission($this_user, 'new_booking_date');
if($this_permission){
$html->datepicker('min' => $this_permission['min'], 'max' => $this_permission['max']);
}
After the booking has been made, another page allows us to edit that field:
// Verify POST data...
$this_permission = get_permission($this_user, 'edit_booking_date');
if($this_permission){
if($this_user == $author_user && $_POST['date'] >= strtotime($this_permission['this_user_min'], $date_ref) && $_POST['date'] <= strtotime($this_permission['this_user_max'], $date_ref)){
// Update database...
} elseif($_POST['date'] >= strtotime($this_permission['other_user_min'], $date_ref) && $_POST['date'] <= strtotime($this_permission['other_user_max'], $date_ref)){
// Update database...
}
}
Am I on the rigt track?
I developed a library called PHP-Bouncer that I think would meet your needs very well. It currently supports fully managed access which will allow you to use a single call on every page (I recommend using an include of course) and automatically redirect people if they don't have access to a page, as well as automatic retrieval of roles from a database (if you implement the roles in the DB using the included MySQL table setup script). The syntax is pretty simple.
You create the bouncer:
$bouncer = new Bouncer();
Add your roles (manually):
// Add a role Name, Array of pages role provides
$bouncer->addRole("Public", array("index.php", "about.php", "fail.php"));
// Add a role Name, Array of pages role provides
$bouncer->addRole("Registered User", array("myaccount.php", "editaccount.php", "viewusers.php"));
// Add a role Name, Array of pages role provides List of pages that are overridden by other pages
$bouncer->addRole("Admin", array("stats.php", "manageusers.php"), array("viewusers.php" => "manageusers.php"));
or from the Database:
// conf_* values are set in a config file, or you can pass them in explicitly
$bouncer->readRolesFromDatabase(conf_hostname, conf_username, conf_password, conf_schema, "mysql");
Add a user and give them some roles (Note: There is a class called BouncerUser which your User class can extend, it provides all of the role functionality you need!):
$user->addRole("Logged In"); // This Role doesn't exist in the bouncer, but we can set it anyways if we feel like setting another flag on the user's account. This can be useful for displaying content in a page only if a user has a secondary role.
$user->addRole("Public");
$user->addRole("Registered User");
Then let the Bouncer manage access to your files:
$bouncer->manageAccess($user->getRoles(), substr($_SERVER["PHP_SELF"], 1), "fail.php");
// Any time the user tries to go to a page they don't have access to, they will get to
// fail.php. Any time they try to go to a page that is overridden for them, they will
// get to the overriding page.
If you want to show content in a page only if a user has permission to view it, simply wrap it in:
if($user->hasRole("Registered User")){
echo "The content";
}
I think for the problem you described this would be a great solution!