Hi,
I want to write ACL. The access groups and access tasks are arbitrary. That means that any group can be given any access and initially accesses and groups are unknown. Creating groups is easy but I have hit a wall on how do I create/Implement limiting the tasks.

Example: Admin group only should be able to delete users. Now the task will be delete user and group permitted is admin but I cannot currently understand how the concept works to limit only admins to access this task.
I use MVC pattern if that is of help.
Thanks!

EDIT:
What I need is how do I limit some controllers and/or methods to given groups?

The admin should be logged in, I am assuming. So in your 'groups' or 'users' you could have a field, something like boolean isAdmin(), or isSuperUser() or, isNormalUser() and set that field when they log in. Then when coding, base your displays (because if they are not admin, they should not be able to even see a delete button) on one (or all) of those values that you have stored in either the session or 'user' class that you are using.
alternatively, if you use classes make a separate 'user', and 'admin' class and only make such functionality available to admin, it can even extend the user class.

The admin should be logged in, I am assuming. So in your 'groups' or 'users' you could have a field, something like boolean isAdmin(), or isSuperUser() or, isNormalUser()

this will work with fixed groups (which I will implement if my theoretical solution I'm seeking will fail) but I want something arbitrary. Admin can create a role and assign tasks. So the tasks as well as groups (names and number of them) are theoretically unlimited. So I want to do something like

bool ACL::HasPermission(TaskObjectOrWhateverYouCallIt, UserGroup);

and use it to check permissions. I can get user group from login credentials no problem. Issue is on how Do I list Controllers/Tasks user is limited to (whitelist).

and set that field when they log in. Then when coding, base your displays (because if they are not admin, they should not be able to even see a delete button) on one (or all) of those values that you have stored in either the session or 'user' class that you are using.

I want to limit access in controller level not view. I want to do all limitations in Front Controller (index.php) using specific ACL object. I will have one page for permission denied eg. PermissionDeniedController where user will be redirected when tried to access restricted resource. Else he should proceed executing the controller he have requested

alternatively, if you use classes make a separate 'user', and 'admin' class and only make such functionality available to admin, it can even extend the user class.

Problem is how do I limit let say EditAllUsersProfile, DeleteAnyUser, ModerateComment et al to a user group, let say Admin?

I'm dreaming that it is possible to put usergroup and corresponding permission on database so that the ACL class itself is not tied directly to groups or permissions

Thanks alot for useful post that have made me think a little further!

In terms of an acl you often have the following entities, Users, Roles, Resources and Privileges. Of course everyone will have their own words for each thing but this is how I would define each for the rest of this post.

User - The Person
Role - The group 1 or more persons are in
Resources - A Noun e.g. newsletter, report, post, etc.
Privileges - The Actions e.g. edit, delete, create, view etc.

So when I have seen acl's applied to the routing it is usually handled in the FrontController via a plugin before the route is dispatched to a controller and action. Which seems to be how want it to function anyways. When this is implemented in this way, generally the Resources is in the pattern of Module.Controller.Action or Module-Controller-Action etc. and the FrontController is looking for a specific Privilege, such as view or access.

The nice thing about this naming convention is it allows you to control access at a granular or broad range. Your frontcontroller would first check the module only, than the module.controller and finally the module.controller.action.

So instead of having to enter records for every action in your system you could simple add the resource admin (admin module) with the access, view etc privilege to the user role admin.

The only thing to keep in mind is by default all users to your site either need a guest role, until they sign in, which allows access to things like login/logout controllers/actions etc or you need to come up with some way to whitelist these. You should also think about role inheritance as this will allow you to only add a limited number of resources per role instead of having to redefine the same resource for multiple roles.

e.g.

ROLE_GUEST would have a resources for your login controller.
ROLE_USER would inherit ROLE_GUEST so it would have those resources as well without having redefine them. This also allows you to add to them as user roles increase.
ROLE_STAFF inherits ROLE_USER which inherits ROLE_GUEST but it adds the user resource (for managing users) with the privileges view, create, edit, delete etc.

Beyond route based ACL this also provides an easy mechanism for checking acl parameters within your actions.

ROLE_USER might have the user resource but only the privilege viewSelf and not viewOther for example.

commented: Great post, thank you! +13

In terms of an acl you often have the following entities, Users, Roles, Resources and Privileges. Of course everyone will have their own words for each thing but this is how I would define each for the rest of this post.

User - The Person
Role - The group 1 or more persons are in
Resources - A Noun e.g. newsletter, report, post, etc.
Privileges - The Actions e.g. edit, delete, create, view etc.

So when I have seen acl's applied to the routing it is usually handled in the FrontController via a plugin before the route is dispatched to a controller and action. Which seems to be how want it to function anyways. When this is implemented in this way, generally the Resources is in the pattern of Module.Controller.Action or Module-Controller-Action etc. and the FrontController is looking for a specific Privilege, such as view or access.

The nice thing about this naming convention is it allows you to control access at a granular or broad range. Your frontcontroller would first check the module only, than the module.controller and finally the module.controller.action.

So instead of having to enter records for every action in your system you could simple add the resource admin (admin module) with the access, view etc privilege to the user role admin.

The only thing to keep in mind is by default all users to your site either need a guest role, until they sign in, which allows access to things like login/logout controllers/actions etc or you need to come up with some way to whitelist these. You should also think about role inheritance as this will allow you to only add a limited number of resources per role instead of having to redefine the same resource for multiple roles.

e.g.

ROLE_GUEST would have a resources for your login controller.
ROLE_USER would inherit ROLE_GUEST so it would have those resources as well without having redefine them. This also allows you to add to them as user roles increase.
ROLE_STAFF inherits ROLE_USER which inherits ROLE_GUEST but it adds the user resource (for managing users) with the privileges view, create, edit, delete etc.

Beyond route based ACL this also provides an easy mechanism for checking acl parameters within your actions.

ROLE_USER might have the user resource but only the privilege viewSelf and not viewOther for example.

All I can say is great post and have given me a good start.
Let me digest on it. you can well point a good tutorial if you have any!

Hi mschroeder,
how do you see this class here? Is it a good way?
I hope no problem with copyrights since it is open and I have linked the source

/**
 * DooAcl class file.
 *
 * @author Leng Sheng Hong <darkredz@gmail.com>
 * @link http://www.doophp.com/
 * @copyright Copyright &copy; 2009 Leng Sheng Hong
 * @license http://www.doophp.com/license
 */

/**
 * Provides Access Control List feature to the application.
 *
 * <p>DooAcl performs authorization checks for the specified resource and action. It checks against the rules defined in acl.conf.php.</p>
 * <p>Only when the user is allowed by one of the rules, will he be able to access the action.
 * If the user role cannot be found in both deny and allow list, he will not be able to access the action/resource</p>
 *
 * <p>Rules has to be defined in this way:</p>
 * <code>
 * # Allow member to access all actions in Sns and Blog resource.
 * $acl['member']['allow'] = array(
 *             'SnsController'=>'*',
 *             'BlogController'=>'*',
 *          );
 *
 * # Allow anonymous visitors for Blog index only.
 * $acl['anonymous']['allow'] = array(
 *             'BlogController'=>'index',
 *          );
 *
 * # Deny member from banUser, showVipHome, etc.
 * $acl['member']['deny'] = array(
 *             'SnsController'=>array('banUser', 'showVipHome'),
 *             'BlogController' =>array('deleteComment', 'writePost')
 *          );
 *
 * # Admin can access all except Sns showVipHome
 * $acl['admin']['allow'] = '*';
 * $acl['admin']['deny'] = array(
 *             'SnsController'=>array('showVipHome')
 *          );
 *
 * # If member is denied, reroute to the following routes.
 * $acl['member']['failRoute'] = array(
 *             //if not found this will be used
 *             '_default'=>'/error/member',
 *
 *             //if denied from sns banUser
 *             'SnsController/banUser'=>'/error/member/sns/notAdmin',
 *
 *             'SnsController/showVipHome'=>'/error/member/sns/notVip',
 *             'BlogController'=>'/error/member/blog/notAdmin'
 *          );
 * </code>
 *
 * <p>You have to assign the rules to DooAcl in bootstrap.</p>
 * <code>
 * # set rules
 * Doo::acl()->rules = $acl;
 *
 * # The default route to be reroute to when resource is denied. If not set, 404 error will be displayed.
 * Doo::acl()->defaultFailedRoute = '/error';
 * </code>
 *
 * @author Leng Sheng Hong <darkredz@gmail.com>
 * @version $Id: DooAcl.php 1000 2009-08-23 20:46:49
 * @package doo.auth
 * @since 1.1
 */
class DooAcl {
        /**
         * Rules settings for the application ACL. Defined in acl.conf.php
         * @var array
         */
        public $rules;

        /**
         * Default route to be reroute to if no custome fail route is defined for a certain rule.
         * @var string|array
         */
        public $defaultFailedRoute = array('/error-default/failed-route/please-set-in-route', 404);

        /**
         * Check if the user Role is in the allowed rule
         *
         * @param string $role Role of a user, usually retrieve from user's login session
         * @param string $resource Resource name (use Controller class name)
         * @param string $action Action name (use Method name)
         * @return bool
         */
        protected function hasAllowed($role, $resource, $action='') {
                if ($action=='') {
                        return isset($this->rules[$role]['allow'][$resource]);
                } else {
                        if(isset($this->rules[$role]['allow'][$resource])) {
                                $actionlist = $this->rules[$role]['allow'][$resource];

                                if ($actionlist==='*')
                                        return true;
                                else
                                        return in_array($action, $actionlist);
                        } else {
                                return false;
                        }
                }
        }

        /**
         * Check if the user Role is allowed to access the resource or action list or both.
         *
         * <code>
         * //Check if member is allowed for BlogController->post
         * Doo::acl()->isAllowed('member', 'BlogController', 'post' );
         *
         * //Check if member is allowed for BlogController
         * Doo::acl()->isAllowed('member', 'BlogController');
         * </code>
         *
         * @param string $role Role of a user, usually retrieve from user's login session
         * @param string $resource Resource name (use Controller class name)
         * @param string $action Action name (use Method name)
         * @return bool
         */
        public function isAllowed($role, $resource, $action='') {
                if (!$this->hasDenied($role, $resource, $action)) {
                        if ($this->hasAllowed($role, $resource, $action)) {
                                return true;
                        }
                }

                return false;
        }

        /**
         * Check if the user Role is denied from the resource or action list or both.
         *
         * @param string $role Role of a user, usually retrieve from user's login session
         * @param string $resource Resource name (use Controller class name)
         * @param string $action Action name (use Method name)
         * @return bool
         */
        protected function hasDenied($role, $resource, $action='') {
                if ($action=='') {
                        return isset($this->rules[$role]['deny'][$resource]);
                } else {
                        if (isset($this->rules[$role]['deny'][$resource])) {
                                $actionlist = $this->rules[$role]['deny'][$resource];

                                if($actionlist==='*')
                                        return true;
                                else
                                        return in_array($action, $actionlist);
                        } else {
                                return false;
                        }
                }
        }

        /**
         * Check if the user Role is denied from the resource or action list or both.
         *
         * <code>
         * //Check if member is denied from BlogController->post
         * Doo::acl()->isDenied('member', 'BlogController', 'post' );
         *
         * //Check if member is denied from BlogController
         * Doo::acl()->isDenied('member', 'BlogController');
         * </code>
         *
         * @param string $role Role of a user, usually retrieve from user's login session
         * @param string $resource Resource name (use Controller class name)
         * @param string $action Action name (use Method name)
         * @return bool
         */
        public function isDenied($role, $resource, $action='') {
                if ($this->hasDenied($role, $resource, $action)) {
                        return true;
                }

                return false;
        }

        /**
         * Check if the user's role is able to access the resource/action.
         *
         * @param string $role Role of a user, usually retrieve from user's login session
         * @param string $resource Resource name (use Controller class name)
         * @param string $action Action name (use Method name)
         * @return array|string Returns the fail route if user cannot access the resource.
         */
        public function process($role, $resource, $action='') {
                if ($this->isDenied($role, $resource, $action) ) {
                        //echo 'In deny list';

                        if (isset($this->rules[$role]['failRoute'])) {
                                $route = $this->rules[$role]['failRoute'];

                                if (is_string($route)) {
                                        return array($route, 'internal');
                                } else {
                                        if (isset($route[$resource])) {
                                                return (is_string($route[$resource]))? array($route[$resource], 'internal') : $route[$resource] ;
                                        } elseif (isset( $route[$resource.'/'.$action] )) {
                                                $rs = $route[$resource.'/'.$action];
                                                return (is_string($rs))? array($rs, 'internal') : $rs;
                                        } elseif (isset( $route['_default'] )) {
                                                return (is_string($route['_default']))? array($route['_default'], 'internal') : $route['_default'];
                                        }
                                }
                        }
                        return $this->defaultFailedRoute;

                } else if($this->isAllowed($role, $resource, $action)==false) {
                        //echo 'Not in allow list<br>';

                        if (isset($this->rules[$role]['failRoute'])) {
                                $route = $this->rules[$role]['failRoute'];

                                if (is_string($route)) {
                                        return array($route, 'internal');
                                } else {
                                        if (isset($route[$resource])) {
                                                return (is_string($route[$resource]))? array($route[$resource], 'internal') : $route[$resource] ;
                                        } elseif (isset( $route[$resource.'/'.$action] )) {
                                                $rs = $route[$resource.'/'.$action];
                                                return (is_string($rs))? array($rs, 'internal') : $rs;
                                        } elseif (isset( $route['_default'] )) {
                                                return (is_string($route['_default']))? array($route['_default'], 'internal') : $route['_default'];
                                        }
                                }
                        }
                        return $this->defaultFailedRoute;
                }
        }
}

It looks relatively simple which is a good thing. However i see a few things that may or may not be an issue for you.

First, it is mixing responsibilities, there really is no reason for an ACL to be handling routing the user. It should only be returning TRUE/FALSE whether the user can do what was queried. If it returns false it should be handled however it needs to outside of the ACL class.

Second, I see no way to deal with inheritance. While this might not be a major sticking point, trust me when I say you will get tired of redefining the same resources and privileges on multiple roles. Especially when you have two roles that are very similar but different by only a couple resources etc.

Another thing to keep in mind, is if you are only dealing with role based acl, you should come up with a way to serialize and cache the acl representation so you're not having to query and build it out each and every time a user hits a page. Once the acl is setup, the only time it would change is when a change was made to the roles/resources/privileges etc., so creating a static representation using php's serialize, json, or an xml representation will make it more efficient.

great comment again!
Thanks. Let me do something and I will be back!

Have a look at the Zend ACL implementations, I think they're similar to what you'll ultimately end up with, even if they only inspire whatever you create. http://framework.zend.com/manual/en/zend.acl.introduction.html

One of the most advanced acl systems I have worked with was built with Zend's ACL at its foundation, but added some layers of functionality on top. Hence my use of their terms.

Have a look at the Zend ACL implementations, I think they're similar to what you'll ultimately end up with, even if they only inspire whatever you create. http://framework.zend.com/manual/en/zend.acl.introduction.html

One of the most advanced acl systems I have worked with was built with Zend's ACL at its foundation, but added some layers of functionality on top. Hence my use of their terms.

It seems Zend and CakePHP are the only framework that are famous AFA framework is concerned.
I will check that!

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.