Simple access control in PHP

Software requiring restriction of certain features needs a mean to positively match the current action with the current user. Some of those means use in-code checks :

against so-called « access-levels », where each user is given a rank (usually an integer), which is compared to a « lowest required access level » for a given action or page ;

by listing allowed users by their username or userid in the script, and checking if the user currently trying to access the current action is amongst those listed;

or a variant of these two ways, using the user’s group(s) membership rather than his specific account.

Those methods are highly clumsy, for multiple reasons :

write access to the scripts is required should someone wish to alter those restrictions ;

they are not adapted to situations with thousands of users and/or dozens of scripts, literally adding hundreds of bytes of data not pertaining to the main logic to the script ;

and quite frankly, they are a right pain to maintain…

Implementing access control in an object-oriented way comes a long way to make your code all the more simple.

This post is written assuming the reader has good notions of OOP in PHP, and is only related to build an access control and what’s directly linked to it.
For anything related to building a login system, I advise you to refer to another tutorial, by ss23, called « Using crypt() ». While it doesn’t provide classes or User class methods to do it, it will certainly give you a more comprehensive understanding of the behaviour of crypt().

It’s written for a (X)AMP solution, MySQL being my database server of choice. But don’t worry, I’m fairly certain it can be adapted for other DBMSes.

I. Database

Let’s assume you already have a working app, with a users table. You need to add a permissions table, that will list all permissions, including their permission_name and a permission_id, and a users_permissions table, that will match any user with any permission he has, using the ids to limit the countless problems a name association could possibly trigger.

Foreign keys will obviously speed the whole thing up, and tidy things up considerably in the event a permission or user is deleted from the database : do not lose time coding in PHP something that can be done in MySQL, especially if it’s going to be twice as fast that way.

CREATE SCHEMA IFNOTEXISTS`mydb` ;

USE`mydb` ;

-- -----------------------------------------------------

-- Table `mydb`.`users`

-- -----------------------------------------------------

CREATETABLEIFNOTEXISTS`mydb`.`users`(

`user_id`INTNOTNULLAUTO_INCREMENT,

`user_name` TINYBLOB NOTNULL,

PRIMARYKEY(`user_id`),

UNIQUEINDEX`user_name_UNIQUE`(`user_name`(255)ASC))

ENGINE = InnoDB;

-- -----------------------------------------------------

-- Table `mydb`.`permissions`

-- -----------------------------------------------------

CREATETABLEIFNOTEXISTS`mydb`.`permissions`(

`permission_id`INTNOTNULLAUTO_INCREMENT,

`permission_name` TINYBLOB NULLDEFAULTNULL,

PRIMARYKEY(`permission_id`),

UNIQUEINDEX`permission_name_UNIQUE`(`permission_name`(255)ASC))

ENGINE = InnoDB;

-- -----------------------------------------------------

-- Table `mydb`.`users_permissions`

-- -----------------------------------------------------

CREATETABLE`users_permissions`(

`user_id`INT(11)NOTNULL,

`permission_id`INT(11)NOTNULL,

PRIMARYKEY(`user_id`,`permission_id`),

KEY`user_id`(`user_id`),

KEY`permission_id`(`permission_id`),

KEY`users_permissions_perm`(`permission_id`),

KEY`users_permissions_user`(`user_id`),

CONSTRAINT`users_permissions_permissions`

FOREIGNKEY(`permission_id`)

REFERENCES`permissions`(`permission_id`)

ONDELETE CASCADE

ONUPDATE NO ACTION,

CONSTRAINT`users_permissions_users`

FOREIGNKEY(`user_id`)

REFERENCES`users`(`user_id`)

ONDELETE CASCADE

ONUPDATE NO ACTION

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

I will let you ALTER the users table on your own if you aleady have one. If you already have one and it is called otherwise, remember to alter the FOREIGN KEY in users_permissions.
Also, that script will work on a database called mydb, make sure you have one by this name and it is the one in use… or adapt the SQL !

II. User class

I tend to like it simple. I also tend to have as few functions and methods as I can without lowering performance, but with the best documentation I can write.
What we need is a way to make sure user X can do action Y. Let’s build a User::can($y) method.
Since we don’t want that method to make too frequent calls to the database (be it a flatfile or actual SQL database, with the latter having my overwhelming preference), let’s also create a User::$permissions property to somewhat cache a user’s permissions.

Code is provided assuming you’re using DB-singletons, with a DBMaster class for write DB access and DBSlave class for readonly DB access, and use PDO as your base DB access abstraction layer. You can of course use it any other way, as long as you use a similar data structure.
I like implicit table joins, but it’s perfectly possible to use explicit joins if you feel better using those instead.

class User {

//I'm assuming here that you already have a User class, once again I will

//only give here code that applies to the ACL

protected$id;

protected$username;

/**

* A list of permissions for this user. Will be filled

* by the first call to the ::can() method.

*/

protected$permissions;

/* ... */

/**

* returns true if the user represented by the object can do the action

That’s as simple as it gets. When first called, that method fetches all permissions currently granted to the user from the database, and stores them in a protected array. With any call, if the requested permission is associated with the user in the database, the method returns true. If not, it returns false.
Furthermore, it is somewhat human-readable. Deleting what pertains to the PHP syntax, it says « if that guy can edit ». Exactly what we mean by that condition. Neat huh ?

However, there is one slight issue : if there is an action that requires multiple permissions, or any of a group of permissions, this method needs to be called several times, and won’t be callable in a simple dynamic context (something like $user->can($array_of_perms)). Not cool.
Well then, let’s say we spice things up a bit :

class User {

protected$id;

/**

* A list of permissions for this user. Will be filled

* by the first call to the ::can() method.

*/

protected$permissions;

/**

* returns true if the user represented by the object can do the action(s)

* given as a param.

* @param permission mixed a string holding a single permission name, or

an array of strings, each holding a permission name

* @param and_or bool if true all permissions must be granted to the user

if false, any of them is sufficient

* @return boolean true if the user can, false if he can't

*/

publicfunction can($permission,$and_or=true){

/*

if an empty string or array is given, it may well be that an

abstraction method was used to check for permission, and we're in the

case where no permission is required to perform the subsequent action

*/

if(empty($permission))returntrue;

/* if $this->permissions isn't set, this method has not been called yet

add a table in your database to log these. This allows for a team of people to check them jointly, for example by DELETEing them from the database once they are checked, or UPDATEing a boolean checked field for the row, etc… ;

send an e-mail to someone (the offender or an administrator), it will attract his attention ; alternatively, you can send a message to any kind of API you wish, I wouldn’t be excessively surprised by a message on Twitter for instance ;

or finally, a combination of several if not all of these ways.

On that, the choice is yours.

Now for a more comprehensive version of permissions-related methods in the User class (without unauthorized access attempts logger) :

/**

* A bogus class representing a user.

* All methods listed here pertain to and only to the access control system

*/

class User {

protected$id;

protected$username;

/**

* An id => name array of permissions currently granted to the user

*/

protected$permissions;

publicfunction __construct($user){

if((is_int($user)||ctype_digit($user))&&$user>0){

if($username= DBSlave::getInstance()

->query("SELECT user_name FROM users WHERE user_id = $user")

->fetch( PDO::FETCH_COLUMN,0)){

$this->id=$user;

$this->username=$username;

}

elsereturn;

}

elsereturn;

}

/**

* returns true if the user represented by the object can do the action(s)

* given as a param.

* @param permission mixed a string holding a single permission name, or

an array of strings, each holding a permission name

* @param and_or bool if true all permissions must be granted to the user

if false, any of them is sufficient

* @return boolean true if the user can, false if he can't

*/

publicfunction can($permission,$and_or=true){

/*

if an empty string or array is given, it may well be that an

abstraction method was used to check for permission, and we're in the

case where no permission is required to perform the subsequent action

*/

if(empty($permission)){

returntrue;

}

/* if $this->permissions isn't set, this method has not been called yet

* Essentially a ban, removes all permissions the current user is currently

* granted with

* @return bool true on success, false on failure

*/

publicfunction ungrantAllPerms(){

$this->permissions=array();

return DBMaster::getInstance()

->prepare('DELETE FROM users_permissions

WHERE user_id = :userid')

->execute(array(':userid'=>$this->id));

}

/**

* Ungrants the current user a given permission

* @param perm mixed a permission represented by name, id, or object

* @return mixed true on success, false on failure

*/

publicfunction ungrantPerm($perm){

if(!$perm= Permission::normalizeToID($perm)){

returnfalse;

}

return DBMaster::getInstance()

->prepare('DELETE FROM users_permissions

WHERE user_id = :userid

AND permission_id = :permid')

->execute(array(':userid'=>$this->id,':permid'=>$perm));

}

}

III. CRUD. The Permission class

No, I’m not cussing. I’m referring to the usual acronym for Create, Read, Update, Delete : all that works fine, per se, but it doesn’t allow a user without DB access to add permissions, or grant them to anyone for that matter.

And the best part : with those neat foreign keys, any record in the users_permissions that matches that permission will also be removed, automatically, without having to take care of it ourselves.
Naturally, if you were to delete a user from your database, all records that match him in users_permissions would have the same fate.

A way to list all currently existing permissions sure would be nice too. Here goes :

class PermissionsList {

protected static $allPerms;

privatefunction __construct(){}

/**

* Returns a list of all permissions currently in the permissions

* table

* @param refresh bool whether or not to refresh the list of permissions.

* @return array an array, each element of which represents a single

* permission with its id as key and name as value

*/

public static function listAllPerms($refresh=false){

if(!isset(self::$allPerms)||$refresh){

self::$allPerms= DBSlave::getInstance()

->query('SELECT permission_id, permission_name

FROM permissions')

->fetchAll( PDO::FETCH_COLUMN| PDO::FETCH_GROUP| PDO::FETCH_UNIQUE);

}

returnself::$allPerms;

}

}

An array of all available permissions is easy to get :

//associative id => name array of all permissions$allPermissions= PermissionsList::listAllPerms();//plain array of all permissions names$allPermissionsNames=array_values( PermissionsList::listAllPerms());//plain array of all permissions ids$allPermissionsIds=array_keys( PermissionsList::listAllPerms());

A complete source code for all three classes (considering User to be a full class, when in fact only methods pertaining to the access control are defined), is available here.

IV. Usage

A class is useless if you don’t use it. Worse than useless, it’s hogging perfectly good memory you could use for something else. Let’s see some use case, shall we ?

IVA. Check for view rights

<?php

//create a User instance for the user currently browsing the script.

$currentUser= User::getCurrent();

//check if the current user has a view permission

if(!$currentUser->can('view')){

header('Location: error.php?perm=view');

return;

}

/* The rest of the code goes here */

IVB. Permissions manager

<?php

//create a User instance for the user currently browsing the script.

$currentUser= User::getCurrent();

//check if the current user can edit permissions

if(!$currentUser->can('giveperms')){

header('Location: error.php?perm=giveperms');

return;

}

if(empty($_GET['permid'])&&empty($_POST['newperm'])){

//let's delete that permission on a POST request

if(!empty($_POST['deleteperm'])){

$deleteperm=new Permission($_POST['deleteperm']);

if($deleteperm->getId()){

$message='The permission you requested for deletion ('.$deleteperm->getName().') has been successfully deleted.';

That 71-line code is a complete secure permissions manager. It lists all your users, allowing to choose one of them from that list, then to edit his permissions, while making sure you have the permission to grant people permissions.

I hope this will be useful to you, feel free to comment — however, comments are moderated, yours may not appear right away : be patient.