Fine-grained access control

Hello folks, I have a question on how would I implement a fine-grained access control mechanism that offers the following:

  1. Users belong to ‘roles’ or ‘groups’ such as ‘salesperson’, ‘planning’, etc.
  2. The admin menu system shows only ‘pages’ which have functions relevant to the users role(s)
  3. Specific 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 and no.2, (Zend ACL, Tony Marsten’s Radicore, etc) but I’d prefer to implement a ‘native’ solution rather than refactoring a framework.

Any ideas how I might build the form control in no.3, 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)

In a bid to implement a ‘native’ solution, rather than piggy-backing a framework, I’ve been playing around with the following 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 right track?

Not to bring, up an old post but I thought it had some good information on it worth containing in a single location. TomB, I really like your idea of using bitmasks, but am fairly unfamiliar with bit code and operations. So I was just trying to do some testing here and was wondering if I’m on the right track.

The below code sets up a group of permissions and loops through them listing whether the user has the permission or not. Is this the right track, am I using the correct bit comparisons?


<?php

$perms = array(

	'ADD_CONTENT' 			=> 110000
	,'ADD_OWN_CONTENT' 		=> 010000
	,'EDIT_CONTENT'			=> 001100
	,'EDIT_OWN_CONTENT'		=> 000100
	,'DELETE_CONTENT'		=> 000011
	,'DELETE_OWN_CONTENT'	=> 000001

);

$users_perms = 011100;

foreach($perms as $name=>$perm) {
	echo "<p>$name: ",(($users_perms & $perm)?'Y':'N'),'</p>';
}

?>

This solution seems a much more elegant than storing constant based permissions in the database. I really like the idea of it all, just need to understand it a little more before I implement it for my own work.

Take a look at what OWASP has done in their ESAPI project.
http://code.google.com/p/owasp-esapi-php/
In particular:
http://owasp-esapi-php.googlecode.com/svn/trunk_doc/latest/ESAPI/AccessController.html (click on Method detail)

Also there is:
http://phpgacl.sourceforge.net/

Summary:
A PHP class offering Web developers a simple, yet immensely powerful “drop in” permission system to their current Web based applications.

Off Topic:

hehe, yes. English is a silly language anyway :wink:

Off Topic:

I hope you mean 512th :stuck_out_tongue:

I have previously (for large sites) split the permissions up into objects and permissions - so you might have ‘add own’, ‘news’ and ‘add own’, ‘comment’ or something. Then I split the users up into groups (well, a user can be a member of multiple groups) and the permissions are applied to the groups - eg ‘Admin’ group can ‘Edit’ ‘News’, so I had a table of user_group_id, permission_id, object_id all of which make up the primary key. Then in my user model I can just write a hasPermission method or something which takes a permission and an object and returns true or false based on their group membership and permissions of those groups.

File permissions work well for restricting access to data, but when you’re dealing with a web application you often need to know more than “Can this user read/write this data?”. Sometimes it’s needed for display logic for example to display a message to a certain subset of users or change the way a process works for certain users (e.g. some users see an additional step when filling out a form).

oddz, that’s similar to what I did. Since then I’m in a different job where we never need anything nearly as complex but I had it set up in such a way that:

-Each section of the site has a set of possible permissions
-Each user can be in any number of groups (Again using bitmasks to store which groups they’re in)
-Each group has a set of permissions for each site module
-Each user can have additional and revoked permissions (again per module) so each user can have unique permissions without needing to make a group for those edge cases

Then, with a simple(ish) query using BIT_OR (which i posted above) that takes userId and moduleId and would return a single number representing the users permissions for any given module.

Then it’s simply a matter of checking the permissions in PHP.

I use a system known as Role-Based Access Control. It’s a pain to write code for, but very very powerful once implemented. This is how I went about it.

Like your sketched out system users have multiple roles. Users can also have multiple groups but one must be defined as primary. Roles define permissions, Groups define ownership - more on this below.

For any given task the user has one of five possible permissions: All Yes, Group Yes, Self Yes, Deny and No Comment.

No trumps yes. So if a user has four roles one of their roles must say yes to the action and none of them should say no. The system obeys the most constraining permissions from among the roles the user has.

As said above, groups define ownership. If a user has more than one group then one of them must be assigned to the file that is being created. The default for this is the primary group. I suppose a resource could have multiple groups too, but if the users get lazy (and they likely will) this could devolve into a situation where all the resources had all the groups and became editable by anyone with multiple groups, defeating the purpose of groups to begin with.

Off Topic:

I just wanted to say that was my 512nd post. How very relevant! :wink:

You’re on the right lines. Generally for storing bits in php i’d use hex to denote the numbers (keeping a running count is easier).

At the moment, your code wont work as 110000 will be treated as decimal not binary.

Try this:


<?php 
class Permissions {

	const ADD_CONTENT = 0x1;
	const ADD_OWN_CONTENT = 0x2;
	const EDIT_CONTENT = 0x4;
	const EDIT_OWN_CONTENT = 0x8;
	const DELETE_CONTENT = 0x10;
	const DELETE_OWN_CONTENT = 0x20;
	const ADD_COMMENT = 0x40;
	const VIEW_COMMENT = 0x80;

	

}


function hasPermission($permissionToCheck, $userPermissions) {
		return $permissionToCheck & $userPermissions;
}

//Give the user some permissions
$myPermissions = Permissions::ADD_CONTENT | Permissions::EDIT_CONTENT | Permissions::VIEW_COMMENT;


//Can the user add content?
if (hasPermission(Permissions::ADD_CONTENT, $myPermissions)) echo 'User can add contnet';
else echo 'User cannot add content';

echo '<br />';

if (hasPermission(Permissions::EDIT_CONTENT, $myPermissions)) echo 'User can edit contnet';
else echo 'User cannot edit content';

echo '<br />';

if (hasPermission(Permissions::DELETE_CONTENT, $myPermissions)) echo 'User can delete contnet';
else echo 'User cannot delete content';


echo '<br />';

//Revoke a permission:
$myPermissions &= ~Permissions::ADD_CONTENT; 

//This should now be false
if (hasPermission(Permissions::ADD_CONTENT, $myPermissions)) echo 'User can add contnet';
else echo 'User cannot add content';

//Add a new permission:
$myPermissions |= Permissions::DELETE_OWN_CONTENT;
?>

Play around with adding/removing flags to $myPermissions.

Hope this is helpful :slight_smile:

edit: I explained bitmasks in some detail here: http://www.sitepoint.com/forums/showthread.php?t=600742

Hmm… Personally when I’m down to file and field level permissions I follow the UNIX bitfields model. I’ll review that at posts end for those not familiar (I assume most readers here are). They are very well tested and proven over the years. We don’t have to worry about an executable permission so that would give us 6 bits to track.

Comments on your permissions though oddz:

ADD - Isn’t this the same as writing to the parent folder or page or whatever? I can’t imagine a situation where someone would need to be able to add files to a page and not be able to edit that page.

DELETE_ALL, DELETE_OWN - Deleting is a write operation is it not? You are just writing the value to null. Again I can’t see someone having permission to edit without permission to delete, especially since an edit can effectively be a delete if they scramble the data.

READ_ALL, READ_OWN - Read as a separate permission from write is spot on. It doesn’t come up often but it’s useful to have that flexibility.

Brief UNIX review follows: Unix permissions have 3 bits for each of three groups - Owner, Group and World. Each resource in the system has an owner and a group. The three permissions are write, read or execute. A PHP permission system likely need not concern itself with executing resources, so it can use a reduced version of this model with 2 bits for each group, total of 6 instead of 9. Hence

  • OWNER_READ
  • OWNER_WRITE
  • GROUP_READ
  • GROUP_WRITE
  • WORLD_READ
  • WORLD_WRITE

World is any user in the system, including a guest in UNIX. An extra pair might make sense to distinguish all authenticated users from all guests if desired.

I appreciate the response TomB, it has helped tremendously.

I’ll explain a little about what I’m trying to accomplish and see if people can suggest an improvement for managing the permission system.

I have a content management framework very similar conceptually to Drupal. At this moment the CMF has a variety of entities within it. Those entities are listed below – though the amount is sure to grow.

  • node types
  • navigation
  • navigation links
  • sites
  • vocabulary
  • terms
  • config

Based upon what I’m seeing and have been researching it seems appropriate to to assume that all these low level entities will have a standard set of permissions:

  • ADD
  • EDIT_OWN
  • EDIT_ALL
  • DELETE_ALL
  • DELETE_OWN
  • READ_ALL
  • READ_OWN

So, I was thinking that all entities would automatically inherit or have these permissions by default.

Now, as far as storing them in the data base my thoughts were to create a table with the below fields.

  • sites_id
  • group [user,role]
  • groups_id
  • entity [node_type,navigation,navigation_link,site,term,vocabulary,…]
  • entity_id (default NULL)
  • permissions (binary rep. of permissions)
  • revoked (binary rep. of user revoked permissions - regardless of users roles)

The idea here being that I can assign permissions to a role or user using a single table. In addition I can go even further and specify an entity id. The entity id would be a foreign key reference to the table that the entity type maps to. For example, there may be a a node type like “private documents”. So I could assign specific permissions to that node type by referencing the node_type entity and its primary key for “private documents” node type. Similarly, I could control permissions on individual terms, vocabularies, etc.

This could be all stored in the single table for users and roles due to having a group column which tells me the permission group - user or role. User being an individual with directly assigned permissions and role being a role that may be assigned to a user. If the group is role than the foreign key groups_id would reference a role, if it is a user it will reference a user. That seems to make things pretty flexible as I could even add an additional layer besides role and user, if needed.

Does all this sound like a decent approach? I’m more familiar with using strictly role based systems in previous projects but am looking for something a lot more flexible here.

The below was actually my original plan, using a standard ACL. However, after thinking about it having been working on other things for a while I don’t think my initial instinct is the best way to about this.


<?php
/*
* Access control list data access layer
*
*/
$this->import('App.Core.DAO');
class MCPDAOACL extends MCPDAO {

private

/*
* In an ideal world this class would be transient
* but storing the permissions here prevents multiple
* trips to database when checking permissions for the current
* user for the current site.
*/
$_arrPermissions;

public function __construct(MCP $objMCP) {
parent::__construct($objMCP);
$this->_init();
}

private function _init() {
/*
* Set current users permissions for the current site
*/
$this->_arrPermissions = $this->getPermissions(
$this->_objMCP->getSitesId()
,$this->_objMCP->getUsersId()
);

}

/*
* Determine whether user has permission to do something or access area
*
* IMPORTANT: This method uses the permissions loaded when class was instantiated. This
* is done to reduce the load on the database but makes way for possible problems when
* updating permissions, roles and/or detecting permissions for other users or based
* on other sites. Due to this one should only use this method for detecting permissions
* of the current logged in user for the current site. Permission checking for other sites
* and users should be done in a separate method (getPermissions). It is safe to use method
* after saving new permission(s) for the current user under the current site because the permission
* cache will be reloaded when those items change. However, that will only occur for the current site
* and current user. Any other type of checking needs to be done else where.
*
* Always remember this method answers this question:
*
* Does the current user have the given permission for the current site?
*
* @param str permission constant
* @return bool yes/no
*/
public function hasPermission($strPermission) {
return in_array($strPermission,$this->_arrPermissions,true); // CASE SENSITIVE !!!
}

/*
* Gets all users permissions (real-time)
*
* @param int sites id
* @param int users id
* @return array users permissions
*/
public function getPermissions($intSitesId,$intUsersId) {

/*
* Build query
*/
$strSQL = sprintf(
'SELECT
r2p.permission
FROM
MCP_ACL_USERS_TO_ROLES u2r
INNER
JOIN
MCP_ACL_ROLES_TO_PERMISSIONS r2p
ON
u2r.roles_id = r2p.roles_id
LEFT OUTER
JOIN
MCP_ACL_USERS_TO_PERMISSIONS u2p
ON
u2r.users_id = u2p.users_id
AND
r2p.permission = u2p.permission
WHERE
u2r.users_id = &#37;s
AND
(u2p.deny IS NULL OR u2p.deny = 0)
GROUP
by
r2p.permission
UNION
SELECT
u2p2.permission
FROM
MCP_ACL_USERS_TO_PERMISSIONS u2p2
WHERE
u2p2.users_id = %1$s
AND
u2p2.deny = 0'
,$this->_objMCP->escapeString($intUsersId?$intUsersId:0) // when user is not logged in return 0
);

/*
* Flatten array for easy in_array() usage to check for permissions
*/
$arrPermissions = array();
foreach($this->_objMCP->query($strSQL) as $arrRow) {
$arrPermissions[] = $arrRow['permission'];
}

/*
* Add magical logged in permission
*/
if($this->_objMCP->getUsersId() && !in_array('LOGGED_IN',$arrPermissions,true)) {
$arrPermissions[] = 'LOGGED_IN';
}

return $arrPermissions;

}

}
?>

This was the wrapper class for the data access object. I think a lot of things here can still be salvaged in regards to the interface naming though the parameters will be changing. This isn’t really even complete, just a mock-up of all the things I thought I would need in terms of a contextual interface for managing permissions.


<?php
/*
* Access Control List for managing user permissions
*/
$this->import('App.Core.Resource');
class MCPACL extends MCPResource {

/*
* Singleton ACL instance
*/
private static

$_objACL;

private

/*
* Access control list data access layer
*/
$_objDAOACL;

public static function createInstance(MCP $objMCP) {
if(self::$_objACL === null) {
self::$_objACL = new MCPACL($objMCP);
}
return self::$_objACL;
}

public function __construct(MCP $objMCP) {
parent::__construct($objMCP);
$this->_init();
}

private function _init() {
/*
* Get ACL DAO
*/
$this->_objDAOACL = $this->_objMCP->getInstance('App.Resource.ACL.DAO.DAOACL',array($this->_objMCP));
}

/*
* Check that a user has permission to do something
*
* @param str permission
* @retrun bool yes/no
*/
public function hasPermission($strPermission) {
return $this->_objDAOACL->hasPermission($strPermission);
}

/*
* Grant permission to user
*
* @param str permission
*/
public function addUserPermission($strPermission) {

}

/*
* Deny user the permission to do something
*
* @param str permission
*/
public function denyUserPermission($strPermission) {

}

/*
* Removes user permission
* NOTE: Not the same as deny
*
* @param str permssion
*/
public function removeUserPermission($strPermission) {

}

/*
* Remove permission from a role
*
* @param str permission
* @param str role
*/
public function removeRolePermission($strPermission,$strRole) {

}

/*
* Add permission to role
*
* @param str permission
* @param str role
*/
public function addRolePermission($strPermission,$strRole) {

}

}
?>

There are cases when users are able to add something but not edit them. Take for example, comments. Its sometimes necessary to not allow edits, and make the person posts “final”. In an actual conversation you wouldn’t be able to go back, so why should what is said on the web be different? That is just meant to be an example, but surely there are cases and it will come up.

Similarly to what I posted previously holds true for removing items. There are cases when you want to allow people to create and edit, but not remove it. In terms of editing being the same as deleting, I can see what you mean. However, I am planning on a revision history. So edit would just create a new version, keeping the old in tact. However, delete would remove the data and its revisions. So there is a significant difference.

In that case I don’t think my purpose is best served using UNIX commands, as they bunch things together and make assumptions that can’t be controlled on a finer level. Such the case is with write, just because someone can write which can really be broken up into three separate permissions add,edit and delete. All of which are necessary to provide the type of fine-grained and flexible architecture that I feel would provide significant benefits.

I’m not really concerned with field and file/content level permissions as I am with content type permissions. The primary purpose of having a entity_id will be referencing a content type, not individual pieces of content, though possible - probably unlikely. Mainly I would like to have permissions to allow, deny, etc on certain content types such as; Blogs, Projects, Products, etc. So that is the main purpose of the entity|entity_id fields, not individual content/file permissions.

Basically yes (there are too many ways to go).

With regards to function get_permission, I recommend to load all current user’s rights in one query (after login). Consecutive queries for each HTML input are expensive.

I use bitmasks for permissions

Then I can do


($user->perms | $user->group->perms) & $permissionToCheck)

Obviously you have to be wary of 31 bit limitations, I have a set of permissions per module, If I need more, I can categorise them into two (or more) sets of 31, I think I’ve only needed this for one section of and site i’ve ever done.

Each user can be in multiple groups which have permissions. These are worked out using mysql’s BIT_OR to get the total permissions of all their groups for a given module, then they can be granted extra permissions per user or denied permissions which the group has.

My query looks like this.


SELECT distinct BIT_OR((IFNULL(gp.funcVal,0) | perms.funcAddVal) & ~perms.funcRemVal) as perm 
			FROMmodule AS m
			Left Outer Join lookup.group_permissions AS gp ON gp.moduleID = m.moduleID AND gp.groupID & :group
			Left Outer Join lookup.user_permissions AS perms ON m.moduleID = perms.moduleID AND perms.UserID = :userid
			WHERE m.name = :module

Sorry dont have time to go into more detail, but it works very well because on a per module basis I can store the users permissions as a single number from this query, which then can be checked against permissions

if ($userperms & ModulePerms::PERMISSION_NAME)

where $userperms is the result of that query and the constant is within one module.

Hi TomB, I played around with the bitmasks and it does seem like quite a quick and powerful way to store permission strings; obviously I can store a human readable version of the permission in a lookup table.

Do you have any pointers as to how the permission peculiarities like “edit_booking” can have both “min = -1 week” and “max = +1 year” for a specific function/module? I’m not sure how I’d seperate out the “edit_booking” permission from the sub-permissions… I guess the permission has permissions???

Bitmasks can only deal with true/false permissions so +1 week/-1 week would have to be their own permissions, e.g. edit_booking_plus, edit_booking_minus

However if it’s not a fixed value (e.g. you can get +2 weeks or -5 weeks) then bitmasks are not the way to go for this permission as they’re not boolean values.

Another way to do this would be a table like:

module_permission which has the fields module id, name, value, userid then you can search by name and module. Of course checking this involves another query if you’re using bitmasks but would allow you to quickly get all the users non-boolean permissions. (I do a similar thing with user settings). You could also store the boolean flags as records in this way, but if you need an administrable system for giving/removing permissions it becomes a more difficult than editing 1 record for module/user/flags.

I would like to hear more about this topic. What other approaches are there besides bitmasks?

I personally haven’t done much with permissions in a while, but the last time I did I basically hardcoded everything, for example: say I have a row in a db table called ‘can_edit_pages’. If the user, through table associations, had a link to that row either directly or through a role they were associated with, then they had access to that. In my code, I then had explicit checks: if($permissions->has($user,‘can_edit_pages’)) … you get the idea.

Obviously that approach does not scale well. Does anyone have a more automated solution?

Been thinking about this a lot. I was planning on placing all permissions for users and roles in a single table. However, the result was a bit messy, with a unique key that spanned 5 columns. So instead I decided it was best to split permissions, between roles and users into two separate tables. Though there is a little repetition, seems like the more viable solution.


roles permissions
------------------------
roles_id
entity_type
entities_id
allow
deny

uk(roles_id,entity_type,entities_id)
    
    
users permissions
------------------------
users_id
entity_type
entities_id
allow
deny

uk(users_id,entity_type,entities_id)
    
roles
------------------------
sites_id
role_name
pkg

uk(sites_id,role,pkg)
       
users roles
------------------------
users_id
roles_id

uk(users_id,roles_id)

I’m thinking once I get this all up and running, all I’m going to need is a single method added to my applications facade. That method will return true/false based on whether the user has the permission.


if( $this->_objMCP->allow(MCP::EDIT,'NAVIGATION_LINK',89) ) {
     ...
}

One question I still have going into this though, is what data type should the deny and allow table be columns be to properly store the binary representations of permissions?

At this point since all entities seem to have a standard set of permissions I’m thininking it may add an unnecessary level of complexity to use bit masks. Perhaps just using a single column for each permission is enough? The value of each would either be 1 or 0.


roles permissions  - MCP_AUTH_ROLES_PERMISSIONS
------------------------
roles_id
entity_type
entity_id
padd
pedit
pdelete
pwrite
pread

uk(roles_id,entity_type,entity_id)
    
    
users permissions   - MCP_AUTH_USERS_PERMISSIONS
------------------------
users_id
entity_type
entity_id
padd
pedit
pdelete
pwrite
pread

One thing I found necessary to add was indeed write. Write will be used to determine whether an item can be created under a certain category. A common example is adding navigation links to a menu. The permission needs to check first whether the person can actually add (write) to the menu. Than it needs to check whether the person can create (add) a link. When adding things, it always seems to be a two step process, which makes write necessary.


$this->allow(MCP::ADD,'NAVIGATION_LINK','NAVIGATION',1);

Can the user add a new link to the navigation menu with a primary key of 1?

That type of thing.