Role Based Access Control in PHP

There are several different approaches when it comes to managing user permissions, and each have their own positives and negatives. For example, using bit masking is extremely efficient but also limits you to 32 or 64 permissions (the number of bits in a 32- or 64-bit integer). Another approach is to use an access control list (ACL), however you can only assign permissions to objects rather than to specific or meaningful operations.

In this article I will discuss my personal favorite approach: role based access control (RBAC). RBAC is a model in which roles are created for various job functions, and permissions to perform certain operations are then tied to roles. A user can be assigned one or multiple roles which restricts their system access to the permissions for which they have been authorized.

The downside to using RBAC is that if not properly managed, your roles and permissions can easily become a chaotic mess. In a rapidly changing business environment, it can be a job in itself to keep track of assigning appropriate roles to new employees and removing them in a timely manner from former employees or those switching positions. Additionally, identifying new roles for unique job duties and revising or removing requires regular review. Failure to properly manage your roles can open the door to many security risks.

I will begin by discussing the necessary database tables, then I’ll create two class files: (Role.php) which will perform a few tasks specific to roles, and (PrivilegedUser.php) that will extend your existing user class. Finally I’ll walk through some examples of how you might integrate the code into your application. Role management and user management go hand in hand, and so in this article I’ll assume that you already have some type of user authentication system in place.

Database

You need four tables to store role and permission information: the roles table stores a role ID and role name, the permissions table stores a permission ID and description, the role_perm table associates which permissions belong to which roles, and the user_role table associates which roles are assigned to which users.

Using this schema, you can have an unlimited number of roles and permissions and each user can be assigned multiple roles.

These are the CREATE TABLE statements for the database:

CREATE TABLE roles (
  role_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  role_name VARCHAR(50) NOT NULL,

  PRIMARY KEY (role_id)
);

CREATE TABLE permissions (
  perm_id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  perm_desc VARCHAR(50) NOT NULL,

  PRIMARY KEY (perm_id)
);

CREATE TABLE role_perm (
  role_id INTEGER UNSIGNED NOT NULL,
  perm_id INTEGER UNSIGNED NOT NULL,

  FOREIGN KEY (role_id) REFERENCES roles(role_id),
  FOREIGN KEY (perm_id) REFERENCES permissions(perm_id)
);

CREATE TABLE user_role (
  user_id INTEGER UNSIGNED NOT NULL,
  role_id INTEGER UNSIGNED NOT NULL,

  FOREIGN KEY (user_id) REFERENCES users(user_id),
  FOREIGN KEY (role_id) REFERENCES roles(role_id)
);

Note the final table, user_role, references a users table which I have not defined here. This assumes that user_id is the primary key of your users table.

You don’t need to make any modifications to your users table to store role information as that information is stored separately in these new tables. Contrary to some other RBAC systems, a user here is not required to have a role by default; instead, the user simply won’t have any privileges until a role has been specifically assigned. Alternatively, it would be possible in the PrivilegedUser class to detect an empty role and respond with a default unprivileged role when required, or you could opt to write a short SQL script to copy over user IDs and initialize them by assigning a default unprivileged role.

Role Class

The primary focus of the Role class is to return a role object that is populated with each roles corresponding permissions. This will allow you to easily check whether a permission is available without having to perform redundant SQL queries with every request.

Use the following code to create Role.php:

<?php
class Role
{
    protected $permissions;

    protected function __construct() {
        $this->permissions = array();
    }

    // return a role object with associated permissions
    public static function getRolePerms($role_id) {
        $role = new Role();
        $sql = "SELECT t2.perm_desc FROM role_perm as t1
                JOIN permissions as t2 ON t1.perm_id = t2.perm_id
                WHERE t1.role_id = :role_id";
        $sth = $GLOBALS["DB"]->prepare($sql);
        $sth->execute(array(":role_id" => $role_id));

        while($row = $sth->fetch(PDO::FETCH_ASSOC)) {
            $role->permissions[$row["perm_desc"]] = true;
        }
        return $role;
    }

    // check if a permission is set
    public function hasPerm($permission) {
        return isset($this->permissions[$permission]);
    }
}

The getRolePerms() method creates a new Role object based on a specific role ID, and then uses a JOIN clause to combine the role_perm and perm_desc tables. For each permission associated with the given role, the description is stored as the key and its value is set to true. The hasPerm() method accepts a permission description and returns the value based on the current object.

Privileged User Class

By creating a new class that extends your existing user class, you can reuse your existing code logic for managing users and then add some additional methods on top of those which are geared specifically towards working with privileges.

Use the following code to create the file PrivilegedUser.php:

<?php
class PrivilegedUser extends User
{
    private $roles;

    public function __construct() {
        parent::__construct();
    }

    // override User method
    public static function getByUsername($username) {
        $sql = "SELECT * FROM users WHERE username = :username";
        $sth = $GLOBALS["DB"]->prepare($sql);
        $sth->execute(array(":username" => $username));
        $result = $sth->fetchAll();

        if (!empty($result)) {
            $privUser = new PrivilegedUser();
            $privUser->user_id = $result[0]["user_id"];
            $privUser->username = $username;
            $privUser->password = $result[0]["password"];
            $privUser->email_addr = $result[0]["email_addr"];
            $privUser->initRoles();
            return $privUser;
        } else {
            return false;
        }
    }

    // populate roles with their associated permissions
    protected function initRoles() {
        $this->roles = array();
        $sql = "SELECT t1.role_id, t2.role_name FROM user_role as t1
                JOIN roles as t2 ON t1.role_id = t2.role_id
                WHERE t1.user_id = :user_id";
        $sth = $GLOBALS["DB"]->prepare($sql);
        $sth->execute(array(":user_id" => $this->user_id));

        while($row = $sth->fetch(PDO::FETCH_ASSOC)) {
            $this->roles[$row["role_name"]] = Role::getRolePerms($row["role_id"]);
        }
    }

    // check if user has a specific privilege
    public function hasPrivilege($perm) {
        foreach ($this->roles as $role) {
            if ($role->hasPerm($perm)) {
                return true;
            }
        }
        return false;
    }
}

The first method, getByUsername(), returns an object populated with information about a specific user. A method almost identical to this will likely already exist in your user class, but you need to override it here so that the PrivilegedUser‘s methods can be called with the appropriate object. If you try to invoke a PrivilegedUser method on a User object, you will get an error stating that the method doesn’t exist.

The second method, initRoles(), uses a JOIN to combine the user_role and roles tables to collect the roles associated with the current user’s ID. Each role is then populated with its corresponding permissions with a call to the Role class method previously created, Role::getRolePerms().

The final method, hasPrivilege(), accepts a permission description and returns true of the user has the permission or false otherwise.

With the preceding two classes in place, checking if a user has a specific privilege is as simple as follows:

<?php
require_once "Role.php";
require_once "PrivilegedUser.php";

// connect to database...
// ...

session_start();

if (isset($_SESSION["loggedin"])) {
    $u = PrivilegedUser::getByUsername($_SESSION["loggedin"]);
}

if ($u->hasPrivilege("thisPermission")) {
    // do something
}

Here the username is stored in the active session and a new PrivilegedUser object is created for that user on which the hasPrivilege() method can be called. Depending on the information in your database, your object output will look similar to the following:

object(PrivilegedUser)#3 (2) {
  ["roles":"PrivilegedUser":private]=>
  array(1) {
    ["Admin"]=>
    object(Role)#5 (1) {
      ["permissions":protected]=>
      array(4) {
        ["addUser"]=>bool(true)
        ["editUser"]=>bool(true)
        ["deleteUser"]=>bool(true)
        ["editRoles"]=>bool(true)
      }
    }
  }
  ["fields":"User":private]=>
  array(4) {
    ["user_id"]=>string(1) "2"
    ["username"]=>string(7) "mpsinas"
    ["password"]=>bool(false)
    ["email_addr"]=>string(0) ""
  }
}

Keeping Things Organized

One of the many benefits of using an OOP approach with RBAC is that it allows you to separate code logic and validation from object specific tasks. For example, you could add the following methods to your Role class to help manage role specific operations such as inserting new roles, deleting roles and so on:

// insert a new role
public static function insertRole($role_name) {
    $sql = "INSERT INTO roles (role_name) VALUES (:role_name)";
    $sth = $GLOBALS["DB"]->prepare($sql);
    return $sth->execute(array(":role_name" => $role_name));
}

// insert array of roles for specified user id
public static function insertUserRoles($user_id, $roles) {
    $sql = "INSERT INTO user_role (user_id, role_id) VALUES (:user_id, :role_id)";
    $sth = $GLOBALS["DB"]->prepare($sql);
    $sth->bindParam(":user_id", $user_id, PDO::PARAM_STR);
    $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT);
    foreach ($roles as $role_id) {
        $sth->execute();
    }
    return true;
}

// delete array of roles, and all associations
public static function deleteRoles($roles) {
    $sql = "DELETE t1, t2, t3 FROM roles as t1
            JOIN user_role as t2 on t1.role_id = t2.role_id
            JOIN role_perm as t3 on t1.role_id = t3.role_id
            WHERE t1.role_id = :role_id";
    $sth = $GLOBALS["DB"]->prepare($sql);
    $sth->bindParam(":role_id", $role_id, PDO::PARAM_INT);
    foreach ($roles as $role_id) {
        $sth->execute();
    }
    return true;
}

// delete ALL roles for specified user id
public static function deleteUserRoles($user_id) {
    $sql = "DELETE FROM user_role WHERE user_id = :user_id";
    $sth = $GLOBALS["DB"]->prepare($sql);
    return $sth->execute(array(":user_id" => $user_id));
}

Likewise, you could add onto your PrivilegedUser class with similar methods:

// check if a user has a specific role
public function hasRole($role_name) {
    return isset($this->roles[$role_name]);
}

// insert a new role permission association
public static function insertPerm($role_id, $perm_id) {
    $sql = "INSERT INTO role_perm (role_id, perm_id) VALUES (:role_id, :perm_id)";
    $sth = $GLOBALS["DB"]->prepare($sql);
    return $sth->execute(array(":role_id" => $role_id, ":perm_id" => $perm_id));
}

// delete ALL role permissions
public static function deletePerms() {
    $sql = "TRUNCATE role_perm";
    $sth = $GLOBALS["DB"]->prepare($sql);
    return $sth->execute();
}

Because permissions are tied directly to the application’s underlying code logic, new permissions should be manually inserted into or deleted from the database as required. Roles on the other hand can be easily created, modified or deleted via an administration interface.

The more roles and permissions you have the more difficult things will be to manage; keeping the lists minimal is important, but sometimes the contrary is unavoidable. I can only advise that you use your best judgement and try not to get carried away.

Summary

You now have an understanding of role based access control and how to implement roles and permissions into an existing application. Furthermore, you’ve learned some tips to help manage your roles and to keep things well organized. As always, I encourage you to experiment and ask questions if you get stuck. We’re all here to learn from each other and I am happy to help when I can!

Image via PILart / Shutterstock

Win an Annual Membership to Learnable,

SitePoint's Learning Platform

  • ellisgl

    “$GLOBALS”, really?

    • http://www.psinas.com Martin Psinas

      Yeah, really. It’s just an example, and you know, it would be much more helpful if you were to offer useful criticism in the form of an alternative method rather than an unhelpful snide remark. I have no problem using GLOBALS in this context; why do you?

      • http://www.rebuy.de Daniel Freudenberger

        The dependencies of your class should be visible from the outside. I don’t want to open your file and read through the source to find which objects i have to write to the registry ($GLOBALS) before I can use your class. IoC should be the way to go – inject the dependencies through setter or constructor injection into your class. __construct(DbAdapterInterface $adapter) { } would be much more elegant.

        • http://www.psinas.com Martin Psinas

          Thanks for that. I agree with your explanation and won’t try making excuses for myself.

      • ellisgl

        While my comment was less filling and “snidish”, but it got the response I wanted. Why did you use “$GLOBALS” in the first place and then to have others chime in with suggestions and comments.

        I still need to look over the code and compare it to what I have and see if I throw some suggestions out there.

    • http://logansbailey.com Logan Bailey

      Perfectly fine comment, a phpmaster knows why they shouldn’t use $GLOBALS. I’m disappointed by sitepoint and phpmasters.com. This is the second example of poor coding I’ve seen on this site in the past couple days. I’d imagine fewer oversites from a site proclaiming to be masters. I think posting articles with easily fixed issues like this leads to the questioning of the credibility of other posts. It endangers the site of becoming another phpclasses.

      • ellisgl

        As readers, we should exercise our fingers to point out issues and offer suggestion so we don’t end up with coders releasing code that is not sound. If the issues are corrected with a note about the correction within the article, I think we can forgive the authors, unlike some site where you have to dig through pages of comments to find fixes and stuff. *coughnetttus+cough*

  • ellisgl

    Here are my suggestions.
    Add an `updated` column as a timestamp. “`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP”. This will allow for simple ‘auditing’ and also will jive better with some DB classes so they can tell if something has been updated.

    Permissions table: Add a “perm_key” as a varchar(30) w/ an index so you can do something like “$user->hasPermission(‘Open Door’);”; instead of having to look it up by id.

    role_perm and user_perm tables: Add a `value` column that as a boolean (tinyint(1)). You can use this to be more “fine grained” in permission setting.

    Add a table called ‘user_perm’ to links to users to individual permissions allowing for more “fine grained” permissions.

    If you are looking for speed in a larger system, you could create another table that could be called `user_compiled_perm`. This would link user ids to permission ids that the user would have, which is what it looks like you are doing now.

  • http://jsmag.com Michael Kimsal

    I’m a little perplexed by the fetchAll(), then explicitly only grabbing the $result[0].
    If you’re only expecting one row, use fetch(). If you actually have more than one result row, it’s probably an error condition that should be dealt with, as it likely violates some logical constraint that your app relies on.

    • http://www.psinas.com Martin Psinas

      Good catch, thanks for pointing that out.

  • http://zaemis.blogspot.com Timothy Boronczyk

    The point of this article is to introduce someone who may not have been acquainted with RBAC before, and to highlight some of the things he/she would need to consider when adding it to an existing non-framework-based application. I don’t want to detract from the original intent of the article.

    With that said, I’ve always viewed PHPMaster as a site not exclusively for “masters,” but also for beginners starting on their journey to “masterhood.” I think there is merit in having articles that target all skill levels. Perhaps what may be missing is a better identification/organization scheme, which I and the other editors will need to discuss.

    I do appreciate everyone who has taken the time to point out problems with this and other articles. The authors and I do our best, but occasionally mistakes happen. Sometimes it’s a typo or a missing image, other times it’s $GLOBALS. We’re not perfect, and it’s good to see the community helping keep us all honest! The important thing is that we all remain polite when such situations arise. Regardless of who it comes from, snark/arrogance begets defensiveness which degrades the conversation rather quickly instead of creating an opportunity to learn and share with each other.

  • http://www.developmentwall.com Mhabub

    I had a custom acl for codeigniter framework.
    Hope to try this in next codeigniter application

    • http://phpcamponline.com Joel Kumwenda

      I like the post, for someone beginning programming this will probably be a very good catch, however for the “MASTERS” there is still more that can be done to improve the code.

  • Hai Dang

    I agree with Timothy. The main purpose of this article is to introduce about Role Based Access Control, how to organize the data. To organize a Role Based Access Control, we will have 4 main tables : Role, Permission, RolePermission and RoleUser. And we should create a class PriviledgeUser extend from User to have a better structure in code. And depending on each framework, we will have our own implementation.

    Thanks Martin Psinas for your article

    • Oreo

      Hi, i’m still reading comments about this example and I have created these tables and php files but I don’t know which structure should have user class to accomplish a right execution of this security module. Thanks for your help!!

  • Kikloo

    hi,

    i am new to php, can you please post an article on classes, how to use them how to create them etc. ? i have tried many step by step tutorials but failed to understand the oop.
    please post.

    thanks.

    • http://www.psinas.com Martin Psinas

      Kikloo, this article was actually supposed to be published AFTER an introduction to classes, but the author of that article got delayed. It’s unfortunate because I wrote the article with an introduction in mind (I was expecting to make a few revisions), and as a result I can imagine it is more difficult to follow.

      I agree with you completely, somebody should write an introduction to classes. I don’t feel confident enough in my OOP knowledge to be the person to do so, however. I’m getting ripped apart for using GLOBALS here, but trying to do it “the right way” just gave me a big headache (neither $this->db or self::$db can be used in static methods, and passing the $db as a parameter feels more ugly than just using GLOBALS to me). I do it this way because it’s easy and it works, but then people will tell you it’s wrong. I wish I could help you more and I hope somebody else will step up to the challenge.

    • Riyaz

      I agree with Kikloo. There are many basic tutorials on OO PHP but they are so brief and the typical “Hello World” style that it does not give one a true explanation of how to use OO PHP in a real world example. I do hope that the authors here would consider putting together a usable tutorial.

  • http://www.tropotek.com.ai/ Mick

    This article was great, a good overview of using RBAC in PHP. I was looking into ACL lists and as you point out, it was not very elegent to control user access permission. I have implemented the RBAC based on your article into my new framework and having the ability to create new roles and assign them to users and permissions gives the webmaster a lot more fine grained control.
    Cheers ;-)

  • Eliezer E. Vargas

    Hi everyone:
    I might sound ignorant, but… Why use a static method to create a instant of the class? Why don’t you put the code to the constructor and create a instant of the class like $u = new class($param); instead? It is just your preference or there is any performance gain or maybe a security issue? Enlightens me please, just a rookie!

  • Stacy

    Cheers. I really appreciate this article and the discussion. I’m an introductory php/mysql coder and this will be saved for reference.

  • kalyan

    hello your tutorial is good but why dont you give download file

  • http://www.fugitz.com Sovello

    Thanks for the great tutorial Psinas.
    I’m a newbie in some concepts, I have found this one cool.
    What is more important to me is the concept and not the code snippets, cos I have my own way of getting things done the OO – way.
    Karibu

  • Bobby Sinnott

    I note in your brief bio that you are self-taught. You really should take some time to study software development patterns, as you have grossly over-complicated what should be a perfunctory, low-overhead task.
    I know this is only an example… But it is a really, really bad one.
    Since you wanted specifics from another gentleman, here’s one to think about: It’s silly to use joins in an RBAC table when you can accomplish the exact same role multiplicity by using to parameters per user: roles (a text word) and values (a scalar quantity).
    Database joins slow things down right when you don’t want to slow things down: at page load.

  • http://www.icomefromthenet.net Lewis Dyer

    If you know the roles that your application will need to support (and its a reasonable number) save yourself alot of work and use the Zend_Acl (http://framework.zend.com/manual/en/zend.acl.html), Hard code your roles as objects (e.g user, admin,customer) and just use a placeholder table, with numeric key id (class name as a primay key).

    I can’t recommend it enough to avoid a many to many relation between user and role, use a role hierarchy instead and keep the relation sane, zend acl can help with that.

  • jay

    Thanks for the article .Hope to see an articele on advanced oops concepts in the future.Thank you

  • Oliverdejohnson

    Thanks dude…all d criticisms notwithstanding, d article is still one of the simplest to understand on the topic.I dont imagine anybody using the code verbatim.only d concept.I definitely love it!

  • Alex.Barylski

    I suppose being pedantic is what being a programmer is all about. That being said, I am surprised by the number of negative comments received. The author (in all fairness) indicated:

    1. role-based-access-control in the title – nothing about best practices, patterns or OO principles, which really would do little for the audience of the article, except convolute and complicate the content for beginners.

    2. Intermediate is the target audience.

    I think some of you have focused on the wrong details. Nothing wrong with playing devils advocate, but a little humility, appreciation and common sense would help. :)

    Regards,
    Alex

  • silaco gilpansa

    Very Useful. Thanks a lot!

  • Sanny

    What will be the permission value will be ,, array or string

  • Muneeb

    Wish i could be that sort of programmer… :( tried… tried… i can code in php… but things just dont seem to advance… you see… i cannot even figure out how to convert to SQL commands to mySQL ones… so this is who i am :(

  • Oreo

    Hi, actually I want to implement this code as a security module into my system and I have already a BD with role an permissions tables, also a users table that it’s working now on production. But I have a question about a php file that I need to implement this solution. I read that I can copy this two files: Role.php and PrivilegedUser.php but this last extends its class of another class called users. I don’t have this class created into my system, and finally i just want to know what should be content of this class so to this example work for me. Thanks

  • https://github.com/silvioprog Silvio Clécio

    Seems a good idea to be implemented in Brook framework: http://brookframework.org. I’ll try your article soon. Thank you buddy! ;)