A Role-Based Access Control (RBAC) system for PHP
By Tony Marston
13th May 2004
Amended 1st May 2014
As of 10th April 2006 the software discussed in this article can be downloaded from www.radicore.org
- What is 'access control'?
- What is 'role based'?
- - Level based
- - User based
- - Group based
- - Responsibility based
- What is a 'menu system'?
- My current design
- Other types of Access Control
- Amendment History
An 'access control' system is just another name for a 'security system' or a 'permissions' system, and in my long career I have been involved in the design and development of several of these systems:
This article will document the features of some of the early systems I worked on and explain the features of my current design.
In a single-user application which is typically found on a desktop computer there is no need for any access control - the user has access to every function within the application. However, in a multi-user application which is deployed over numerous devices which are linked together in a network it is more than likely that not all functionality will be available to all users. In this situation a method is required whereby functions within the application can only be accessed by persons to whom permission has been granted. This will typically require the maintenance of the following details:
- A list of all the functions that are available within the system. These 'functions' are sometimes referred to as 'transactions' or 'tasks'.
- A list of all the persons who are allowed to access the application as a whole. These 'persons' are sometimes referred to as 'users'. Typically this information is used in a logon process through which every user must pass before gaining access to any part of the application.
- A list of permissions which identifies which functions are accessible by which users.
Each of these lists is normally maintained as a table within a database.
There is more than one way to give different permissions to different users, but each method has its own set of advantages and disadvantages. Here are some that I have encountered:
This is a simple system as it only requires two database tables - USERS and TASKS - without any relationship between them, as shown in Figure 1:
Figure 1 - a level-based permission system
In this system each TASK is given a security level number in the range 1 to 99, with 1 being the lowest level and 99 the highest. Each USER is then given a security level number and is allowed to access only those TASKs which have a security level which is the same or lower. Thus a USER with a security level of 5 can access a TASK which has a security level in the range 1-5.
The problem with this system is that it is totally cumulative - by raising the level number you can add more tasks, and you can only remove tasks by reducing the level number. Groups of tasks that share the same level number are either included or excluded as a group, there is no possibility to mix'n'match. For example, take a simple setup with two users, 'A' and 'B', and two tasks, 'A' and 'B'. Now try to give user 'A' access to task 'A' but not task 'B', and user 'B' access to task 'B' but not task 'A'. You will find that it cannot be done:
- If both tasks have the same security level then access can be granted to both or neither.
- If one task has a lower security level than the other then access can be granted to the lower level on its own, or to both levels. It is not possible to grant access to the higher level and exclude tasks at a lower level.
In this system permissions are defined for individual users. This involves a many-to-many relationship between USERS and TASKS with PERMISSIONS being the link or intersection table, as shown in Figure 2.
Figure 2 - a user-based permission system
I have seen several different implementations of this design:
- In a system with complex tasks - where a single tasks can operate in create, read, update and delete mode - access to a task will include all of those modes.
- Where access to individual modes within a task is required then the PERMISSIONS record needs to have a YES/NO switch against each one of those modes. This is often referred to as a CRUD matrix (where 'CRUD' stands for Create, Read, Update and Delete) as the arrangement of tasks rows and permission columns resembles a matrix.
- In a system with simple tasks - where each of the modes is handled by a totally separate task - then a CRUD matrix is not required.
My personal preference is for a system to be comprised of small and simple tasks. This may increase the number of tasks, but each one is simple, easer to design, easier to specify, easier to develop and easier to use.
This disadvantage of this design is that where several users share the same permissions any change to those permissions needs to be repeated for each user.
In this design the users are split into groups and permissions are assigned to the group, not the individual user, as shown in Figure 3.
Figure 3 - a group-based permission system
This design has the following advantages:
- Once the user has been identified the USER record will supply the USER-GROUP identity which is all that is needed to access the PERMISSIONS table.
- Any change made to a group's permissions will automatically be inherited by all members of that group
- Changes to a group's permissions can be made very easily as there is only one table, the PERMISSIONS table, to maintain.
- If an individual user is switched to another group this will sever all connections to the permissions of the previous group and replace them with those of the new group.
In this design the USER-GROUP table is sometimes known as SECURITY-CLASS or ROLE.
In this design it is possible for a user to belong to more than one group at the same time. This involves two many-to-many relationships, as shown in Figure 4.
Figure 4 - a responsibility-based permission system (simple)
The USER-GROUP table is sometimes referred to as AREA-OF-RESPONSIBILITY because an individual user may have responsibilities in more than one area.
This design has the following disadvantages:
- It is only possible to add permissions by linking a user to another user group. It is not possible for the addition of another group to undo any permissions granted by an existing group.
- There are now two tables to maintain in order to give a user access to a task - the USER-USER-GROUP table and the TASK-USER-GROUP table.
A more complex version of this design is shown in Figure 5.
Figure 5 - a responsibility-based permission system (complex)
In this design there are now five many-to-many relationships which enables a far wider range of customisation. In the implementation I saw the tasks were complex (a single task could operate in Create, Read, Update and Delete mode) which meant that each of the link/intersection tables was a CRUD matrix and could either turn a set of options ON (as an "include" list) or OFF (as an "exclude" list). These tables were read in a strict sequence and the task permissions on one table could be updated by the task permissions on another table. It was therefore possible for a record with a permission checked ON to be superseded by a record from another table with that permission checked OFF.
Even though in theory the above design appeared to be much more flexible, in practice this created a problem with usability. As permissions can exist on five tables, and the permission granted on one table can be taken away by the contents of another table it becomes a more difficult process to track down which user has access to which task.
Within an application that may contain dozens or even hundreds of functions a method is required whereby the user can quickly activate the function that will allow him to perform the desired task. In some of the first systems I worked on in the 1970s this 'list' was actually nothing more than a written list, and each part of the system had to be activated manually from the command line. A later innovation was to have this list presented on the computer screen with a mechanism so that simply selecting an item would cause it to be activated. With large systems it was cumbersome to have this list presented in a single unit, so it was split into smaller units or pages. These pages were arranged in a hierarchical structure, and links on one page could activate other pages at a lower level. This was the birth of the menu system.
In the first systems I worked on the menu pages were always hard-coded in a fixed and rigid structure. This method had the following disadvantages:
- The menu pages had to be designed and constructed before development could start.
- The menu pages could not be altered without changing and recompiling a piece of program code.
- Every user always started at the same menu page after logging on, even if the only options which they were permitted to access existed on a sub-menu.
- Options which were not accessible to a user were still visible on the menu screens.
- Context could not be passed from one screen to another. This meant that if the user found a record on one screen and wanted to go to another screen to see more details the record identity had to be entered manually into the second screen. This was slow and frustrating work, especially if a mistake was made when keying in the record identity.
In the mid-1980s I was asked to design a menu system which did not include these disadvantages, so I came up with the following:
- I added a 'menu' table to the security database so that menu screens could be constructed dynamically from the contents of the database. As the database already contained a list of transactions for security purposes I could use the same data in the definition and construction of menu screens.
- This meant that menu pages could be constructed and modified without the need for programmer intervention. There was no need to define the entire menu structure at the start of the project as it could be modified 'on the fly'.
- Where a user only had permission to access functions which existed on a sub-menu I allowed that sub-menu to appear immediately after the logon screen, thus bypassing all unnecessary menu screens.
- As the menu screens were constructed from the database, and as the database also contained the user permissions, it was a simple matter to filter out all those options to which the user did not have permission to access. Instead of having to build a separate set of menu screens for each user I could build a single set which could be shared by all users, and each user would only see those options which he was permitted to access.
- I created a mechanism whereby context could be passed from one screen to another, thus removing the need to enter record identities manually.
As you can see a security system and a menu system share a lot of common data, so it makes sense (to me at least) to combine them into a single system.
My current design has evolved over 20 years and has been written in three different languages. It has been used as the basis for many different systems for many different clients and has proved to be effective and robust. As it is driven by the contents of the database then changes can be made easily and quickly. Due to the modular design any changes in functionality can be made easily either by changing an existing module or by adding in a new module.
I originally chose to implement group based security around the USER<==ROLE==>ROLE TASK<==TASK tables as this gave sufficient flexibility with a simple set of options:
- Permissions for each Role and Task can be maintained on a single screen.
- A User's single Role can be maintained on the 'Update User' screen.
- At run-time permission can be verified with a single lookup on the ROLE-TASK table using a ROLE_ID and a TASK_ID.
In 2014 I decided to upgrade this to implement responsibility based security around the USER==>USER ROLE<==ROLE==>ROLE TASK<==TASK tables as this provides the ability to link a User to more than one Role with only a slight increase in complexity:
- Permissions for each Role and Task can continue to be maintained on a single screen.
- A User's list of Roles needs to be maintained on a separate USER-ROLE table. One of these Roles must be marked as the primary Role for that User.
- At run-time permission can still be verified with a single lookup on the ROLE-TASK table, but using a list of the User's ROLE_IDs (instead of just a single ID) and a TASK_ID.
Here is an explanation of the other tables in my database:
- PATTERN - Each task conforms to one of the patterns in Transaction Patterns for Web Applications, and I have found it very useful to have this as an identifier on each TASK record. For example, when selecting entries for the ROLE-TASK (PERMISSIONS) table I can very quickly isolate all the tasks of any particular type.
- SUBSYSTEM - Each application or system can often be broken down into discrete parts (or subsystems) which can be regarded as separate collections of components. For example, 'Menu and Security' is separate from 'Workflow' which is separate from 'Product' which is separate from 'Customer'. It is quite often that a user will have responsibility in only one of these areas, therefore it is useful to have this as part of the selection criteria when maintaining access permissions.
- MENU - Entries on the TASK table which are of type 'MENU' require to have their contents maintained on the MENU table. When a user selects a menu the contents of the MENU table can be retrieved and displayed. By using the entries on the ROLE-TASK table any MENU option which is not accessible to the user can be filtered out, thus restricting the display to only those options which the user is actually allowed to access. The contents of the MENU table is displayed in the menu bar area of my web pages.
- NAVIGATION BUTTON - There are some tasks which can be accessed without the need for any context being supplied, and these can be made available on any MENU screen. However, in my infrastructure I have forms families which are comprised of a parent form and several child forms. While the parent form may appear on any MENU, the child forms cannot be made available until the parent form is active. This is because the child forms require context, and context is supplied by the parent form. For example, you must select one or more entries in a LIST form before you can pass control to an UPDATE, ENQUIRE or a DELETE form. These child tasks are defined on their own database table, and are displayed in their own navigation bar area on the web page.
- TASK-FIELD and ROLE-TASK-FIELD - I have sometimes been asked to provide a version of a screen which has access to individual fields downgraded to 'read-only' or even 'invisible'. In some languages it may not be possible to dynamically change the way a field is painted on the screen, in which case it is usually necessary to create a completely different version of the screen. However, as each web page is an HTML document which is built from an XSL transformation I have devised a mechanism whereby I can pass instructions to the transformation process to alter the way that individual fields are to be handled. By default each user has FULL access to every field on the screen, but this can be altered by first defining the 'special case' fields on the TASK-FIELD table, then by altering the field permissions on the ROLE-TASK-FIELD table. This means that a single screen can be viewed by different users with different capabilities and the field definitions will be altered dynamically.
- HELP-TEXT - Within the screen for each task is a hyperlink which will bring up a separate page of help text for the current task. This is retrieved from the database and made available to the user through a standard 'help' function. This help text may contain hyperlinks to other documents.
Some people may say that having to maintain such a complex database is overkill for such a 'simple' requirement, but in my long experience the customer is always dreaming up 'little additions' to his requirements that cannot be satisfied with a primitive design. How easy would it be for you to change your current system to incorporate the following 'little requirements':
- Can I have dynamic menus which I can easily modify instead of ones that can only be changed by modifying program code?
- When a menu is displayed I only want to see the options that I can actually access.
- Within a single screen I want to change access to certain fields for certain users.
I have also found over the years that by having such a database I can easily extend it to incorporate new requirements or features. For example, because of the TASK table I am able to include extra pieces of information which can be used by the task at run-time and which can be changed without going near any source code, such as the initial sort order for LIST screens.
All the maintenance screens for the PHP+MySQL version of my Menu and Security system are documented in User Guide to the Menu and Security (RBAC) System.
The system described in this document grants or denies permission to use particular tasks within an application, and if a user is granted access to a particular task then he has automatic access to all the data which can be handled by that task. For example, permission to use the "Update Customer Details" task means that the user can update the details of any customer.
Some users may require a level of permissions which goes beyond the individual task and is applied at the data level, such as the following:
- An application database usually holds the data for a single organisation, and all the users belong to that organisation, so all the users have access to all the data (subject to the tasks to which they have permission). In some cases the database may deal with several organisations, but each user can only access the data for the organisation to which he belongs. For example, a user of organisation #1 has access to the "Update Customer Details" task, but can only use it to access the data for organisation #1. The data for all other organisations is totally invisible to him.
This requirement, which is sometimes known as a Virtual Private Database (VPD) or Row Level Security (RLS), can be implemented in the Radicore framework by following the instructions contained within Implementing Virtual Private Databases.
- A variation of the above is where a user can access the data for any organisation, but has different permissions within each organisation. For example, he may have permission to use the "Update Customer Details" task within organisation #1, but not for organisation #2.
This variation could be implemented within the existing framework by each user having a separate logon for each account/organisation.
If having a separate logon for each account/organisation is not acceptable, this would require a change to the framework so that each user could have a different set of permissions for each account/organisation, and to modify the LOGON screen to include a field to identify which account/organisation is going to be accessed.
© Tony Marston
13th May 2004