
Access control system on cakePHP.
As you know, there are various access control systems.
Some of them are simple, implemented only on the basis of sessions, while others are complex, such as ACLs. Each of them has its pros and cons. Simple systems are easy to understand and easy to use, but with an increase in the number of privileges and the need to dynamically change them, corresponding difficulties will arise, while ACLs are rather cumbersome, not so flexible and difficult to understand. For a long time using both systems, I came to the conclusion that we need to develop our own access control system, which would have the following features:
While reading existing topics about access sharing systems, I often came across a comment: “Why fence a garden if there is an ACL”. Immediately I will answer what ACL does not suit me.
And so let's start with the theory.
Table structure

Description of the
Users table - everything is simple here, this table is used to store users, the key fields for us are id, lgn, pwd. In this table, the first entry with id = 0 will be the default user, so to speak visitor.
Groups are groups (e.g. User, Admin, Manager)
Statuses- Each group consists of statuses. Roughly speaking, each user is included in a particular group with a certain status. A user can be in several groups with different statuses, but within a single group a user can have only one status. For example, the following groups have the status User - visitor, active, new; Admin - active, superadmin, deleted; Manager - active, blocked; Visitor (id = 0) will belong to the User group and be in visitor status. Each group has a default status - defstats_id - other statuses can inherit access rights from the default status, and may have their own rights to a specific object.
Users_statuses - table for binding user and status
Objects- stores all security objects (for example, the registration button can be the security object, which is displayed only by visitors)
objects_categories - the very first experience in operating this system showed that security objects need to be organized somehow. For this, an additional table was introduced, which serves only to organize (when displaying) security objects. For example, among the security objects we will have such categories - default - all new objects fall into this category (and then the administrator transfers the object to another category), buttons - all objects associated with buttons, links - objects associated with links and so on, as far as your imagination and task allows you.
Access- This is the key table of our system. It links security objects, statuses and privileges. This table has five main columns for differentiating the type of requested access:
c - access to create (for example, checking access to create a new blog entry)
u - access to change (for example checking access to edit a blog entry)
r - read access (for example, checking access to read a blog entry)
d - access to delete (for example checking access to delete a blog entry)
l - access to display a list (for example, checking access to show all blog entries or showing all users)
These fields can take l the following values:
0 - access is denied
1 - access is allowed only to the owner
2 - access is allowed to all
. This table describes the default statuses of each group, all other statuses, inherit access rights from the default status of their group. If the status should have its own access rights (well, for example, the blocked status has all the rights of the default status of active, except that it cannot add new blog entries), then a line with the corresponding status_id is added to the access table.
how the system works
let's analyze the logic of the system. Access verification will be performed by the getAccess function (object, access, owners);
function parameters
object - the name of the object for which access is requested .;
access - type of access (can be with, u, r, d, l);
owners is an optional parameter, which can be either a value of type int - id of the user, or a list of id of users. At the moment, leave this optional parameter unattended - later it will become clear how it is used by the system.
It is worth making a reservation that we always know the id of the current user - we store it in the session, for an unauthorized user - this is a user with id = 0 and consisting of a user group with the status visitor.
And so how does the system work.
1. check if there is an object with this name in the objects table, if not, create and remember its id
2. from the session we take the id of the current user and look at the statuses of this user (in implementation, the status id of the current user is stored in the session)
3. according to the desired type of access and depending on the system settings (weak, strong) we take max or min values for the desired access field (r, c, d, u, l) for the corresponding statuses. If the status is not in the access table, then we take def_status of the corresponding group.
4. Possible results - 0 access is denied, 2 - access is allowed to all, 1 - access is allowed only to owners. If access is allowed only to owners then:
check whether the last parameter of the checkAccess function is empty or not, owners, if it is empty then allow / disable access (optional). If it is not empty and of type int, then we check it with the user id from the session (current user). If they are equal, then access is allowed, otherwise it is denied. If the owners parameter is an array, then we check to see if the id of the current user (from the session) enters this array (in_array), if there is an entry, then access is otherwise denied.
That's the whole algorithm of work.
Usage Example
Let us have a blog post created by user creator_id. We want to display the “edit record” button for some users and hide it for others.
Everything is very simple if (checkAccess ('BlogPost', 'u', $ post ['creator_id'])) {echo BUTTON}
For example, the user is in two groups and the statuses have the following privileges on Update
User \ active -1;
Admin \ active -2;
according to the system settings, we take max (or min) and return the result.
Implementation.
Implemented a cakePHP system with query caching as a component. It is enough to cache the four main tablets: access, statuses, groups, objects, so that there is not a single request when checking access. (these plates are small, so they won’t take up much space and we can allow them to be cached as a whole). Several controller / model / view were also used - purely for visualizing system control.
We load the tablets into the cache (cake allows us to use two types of cache: file and memcached, so we certainly will not stay without the cache on any servers).
For convenience, we need to cache so that the arrays look like this:
Access
permissions [status_id] [object_id] [access_type] = permission;
example:
permissions [status_id] [object_id] [s] = 1;
permissions [status_id] [object_id] [d] = 1;
permissions [status_id] [object_id] [r] = 1;
permissions [status_id] [object_id] [u] = 1;
permissions [status_id] [object_id] [l] = 1;
Statuses
statuses [status_id] = group_id that is, each status stores in which group it is
Groups
groups [group_id] = def_status_id that is, each group stores its default status
Objects
object [object_name] = object_id
Implementation of the main functions:
that’s actually all, if anyone needs the component itself, I can provide it. Access control visualization is shown in the figure below

, groups, statuses and types of access are displayed vertically. default statuses are highlighted in red. Horizontally there are objects, immediately below them is a list of categories for transfer. checkbox - to indicate that access for this object for this status will be the same as for default status of this group. (for them we set - // -). access to men using AJAX, i.e. without rebooting (while updating the cache)
Some of them are simple, implemented only on the basis of sessions, while others are complex, such as ACLs. Each of them has its pros and cons. Simple systems are easy to understand and easy to use, but with an increase in the number of privileges and the need to dynamically change them, corresponding difficulties will arise, while ACLs are rather cumbersome, not so flexible and difficult to understand. For a long time using both systems, I came to the conclusion that we need to develop our own access control system, which would have the following features:
- ease of understanding and ease of execution
- dynamic co-creation of groups / roles and user movement by roles / groups
- the user can be in any number of groups \ roles
- easy, quick and easy access change
- code minimization when using the system
- minimization of the size of tables related to access control
- minimizing the number of database queries
While reading existing topics about access sharing systems, I often came across a comment: “Why fence a garden if there is an ACL”. Immediately I will answer what ACL does not suit me.
- difficulty understanding
- the user cannot be in several groups at the same time
- if it is necessary to restrict access to individual users, tablets are inflated (it is necessary to store the id of each user)
- difficulty with moving users in groups
- lack of caching
- the need to do visualization to work with groups and rights
- own vest is closer to the body (in the sense that your code is easier to hold)
And so let's start with the theory.
Table structure

Description of the
Users table - everything is simple here, this table is used to store users, the key fields for us are id, lgn, pwd. In this table, the first entry with id = 0 will be the default user, so to speak visitor.
Groups are groups (e.g. User, Admin, Manager)
Statuses- Each group consists of statuses. Roughly speaking, each user is included in a particular group with a certain status. A user can be in several groups with different statuses, but within a single group a user can have only one status. For example, the following groups have the status User - visitor, active, new; Admin - active, superadmin, deleted; Manager - active, blocked; Visitor (id = 0) will belong to the User group and be in visitor status. Each group has a default status - defstats_id - other statuses can inherit access rights from the default status, and may have their own rights to a specific object.
Users_statuses - table for binding user and status
Objects- stores all security objects (for example, the registration button can be the security object, which is displayed only by visitors)
objects_categories - the very first experience in operating this system showed that security objects need to be organized somehow. For this, an additional table was introduced, which serves only to organize (when displaying) security objects. For example, among the security objects we will have such categories - default - all new objects fall into this category (and then the administrator transfers the object to another category), buttons - all objects associated with buttons, links - objects associated with links and so on, as far as your imagination and task allows you.
Access- This is the key table of our system. It links security objects, statuses and privileges. This table has five main columns for differentiating the type of requested access:
c - access to create (for example, checking access to create a new blog entry)
u - access to change (for example checking access to edit a blog entry)
r - read access (for example, checking access to read a blog entry)
d - access to delete (for example checking access to delete a blog entry)
l - access to display a list (for example, checking access to show all blog entries or showing all users)
These fields can take l the following values:
0 - access is denied
1 - access is allowed only to the owner
2 - access is allowed to all
. This table describes the default statuses of each group, all other statuses, inherit access rights from the default status of their group. If the status should have its own access rights (well, for example, the blocked status has all the rights of the default status of active, except that it cannot add new blog entries), then a line with the corresponding status_id is added to the access table.
how the system works
let's analyze the logic of the system. Access verification will be performed by the getAccess function (object, access, owners);
function parameters
object - the name of the object for which access is requested .;
access - type of access (can be with, u, r, d, l);
owners is an optional parameter, which can be either a value of type int - id of the user, or a list of id of users. At the moment, leave this optional parameter unattended - later it will become clear how it is used by the system.
It is worth making a reservation that we always know the id of the current user - we store it in the session, for an unauthorized user - this is a user with id = 0 and consisting of a user group with the status visitor.
And so how does the system work.
1. check if there is an object with this name in the objects table, if not, create and remember its id
2. from the session we take the id of the current user and look at the statuses of this user (in implementation, the status id of the current user is stored in the session)
3. according to the desired type of access and depending on the system settings (weak, strong) we take max or min values for the desired access field (r, c, d, u, l) for the corresponding statuses. If the status is not in the access table, then we take def_status of the corresponding group.
4. Possible results - 0 access is denied, 2 - access is allowed to all, 1 - access is allowed only to owners. If access is allowed only to owners then:
check whether the last parameter of the checkAccess function is empty or not, owners, if it is empty then allow / disable access (optional). If it is not empty and of type int, then we check it with the user id from the session (current user). If they are equal, then access is allowed, otherwise it is denied. If the owners parameter is an array, then we check to see if the id of the current user (from the session) enters this array (in_array), if there is an entry, then access is otherwise denied.
That's the whole algorithm of work.
Usage Example
Let us have a blog post created by user creator_id. We want to display the “edit record” button for some users and hide it for others.
Everything is very simple if (checkAccess ('BlogPost', 'u', $ post ['creator_id'])) {echo BUTTON}
For example, the user is in two groups and the statuses have the following privileges on Update
User \ active -1;
Admin \ active -2;
according to the system settings, we take max (or min) and return the result.
Implementation.
Implemented a cakePHP system with query caching as a component. It is enough to cache the four main tablets: access, statuses, groups, objects, so that there is not a single request when checking access. (these plates are small, so they won’t take up much space and we can allow them to be cached as a whole). Several controller / model / view were also used - purely for visualizing system control.
We load the tablets into the cache (cake allows us to use two types of cache: file and memcached, so we certainly will not stay without the cache on any servers).
For convenience, we need to cache so that the arrays look like this:
Access
permissions [status_id] [object_id] [access_type] = permission;
example:
permissions [status_id] [object_id] [s] = 1;
permissions [status_id] [object_id] [d] = 1;
permissions [status_id] [object_id] [r] = 1;
permissions [status_id] [object_id] [u] = 1;
permissions [status_id] [object_id] [l] = 1;
Statuses
statuses [status_id] = group_id that is, each status stores in which group it is
Groups
groups [group_id] = def_status_id that is, each group stores its default status
Objects
object [object_name] = object_id
Implementation of the main functions:
function getAccess($objName = "",$accessType = "r",$authorID=NULL) {
$objectID = 0;
$isAccess = true;
/*Getting User ID*/
if ($this->Session->check('loggedUser')) {
$userSession = $this->Session->read('loggedUser');
$userID = $userSession['id'];
} else {
$userID = VISITOR_USER;
}
/*Check access
* 0 - deny;
* 1 - allow only for author;
* 2 - allow for ALL;
*/
$isAccess = $this->__returnAccess($objName,$accessType);
if ($isAccess == 2){
$isAccess = true;
} elseif($isAccess == 1) {
/*Check author id*/
if (is_array($authorID) && in_array($userID,$authorID)){
$isAccess = true;
} elseif($userID==$authorID){
$isAccess = true;
} else {
$isAccess = false;
}
/*EOF Checking author id*/
} else {
$isAccess = false;
}
return $isAccess;
}
основная функция, которая возвращает 1,0 или 2
function __returnAccess($objName = "",$accessType = "r"){
if (!$this->model){
$this->__initModel();
}
/*Getting User ID*/
if ($this->Session->check('loggedUser')) {
$userSession = $this->Session->read('loggedUser');
$userID = $userSession['id'];
} else {
$userID = VISITOR_USER;
}
/*Getting user statuses*/
if ($this->Session->check('loggedUserStatuses')) {
$userStatuses = $this->Session->read('loggedUserStatuses');
} else {
$userStatuses = $this->model->query("SELECT user_id, status_id FROM ".$this->model->tablePrefix."users_statuses AS users_statuses WHERE user_id=".$userID);
$this->Session->write('loggedUserStatuses',$userStatuses);
}
$objectID = $this->getObjIdByName($objName);
if (!$objectID) {
/*Create new object*/
$objectID = $this->__createNewObject($objName);
}
//Permissions
$permissions = Cache::read('permissions');
if (empty($permissions)) {
$this->loadobjToCache();
$permissions = Cache::read('permissions');
}
//Groups
$groups = Cache::read('groups');
if (empty($groups)) {
$this->loadobjToCache();
$groups = Cache::read('groups');
}
//Statuses
$statuses = Cache::read('statuses');
if (empty($statuses)) {
$this->loadobjToCache();
$statuses = Cache::read('statuses');
}
$isAccess = 0;
foreach ($userStatuses as $userStat) {
if (isset($permissions[$userStat['users_statuses']['status_id']][$objectID] [$accessType])) {
if (intval($permissions[$userStat['users_statuses']['status_id']][$objectID][$accessType])>$isAccess) {
$isAccess = intval($permissions[$userStat['users_statuses']['status_id']][$objectID][$accessType]);
}
} else {
/*Getting group ID*/
$def_status_id = $groups[$statuses[$userStat['users_statuses']['status_id']]];
if (!isset($def_status_id)) {
$isAccess = 0;
} else {
if (intval($permissions[$def_status_id][$objectID][$accessType])>$isAccess) {
$isAccess = intval($permissions[$def_status_id][$objectID][$accessType]);
}
}
}
}/*EOF foreach*/
return $isAccess;
}
* This source code was highlighted with Source Code Highlighter.
that’s actually all, if anyone needs the component itself, I can provide it. Access control visualization is shown in the figure below

, groups, statuses and types of access are displayed vertically. default statuses are highlighted in red. Horizontally there are objects, immediately below them is a list of categories for transfer. checkbox - to indicate that access for this object for this status will be the same as for default status of this group. (for them we set - // -). access to men using AJAX, i.e. without rebooting (while updating the cache)