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.

  1. CREATE SCHEMA IF NOT EXISTS `mydb` ;
  2. USE `mydb` ;
  3.  
  4. -- -----------------------------------------------------
  5. -- Table `mydb`.`users`
  6. -- -----------------------------------------------------
  7. CREATE  TABLE IF NOT EXISTS `mydb`.`users` (
  8.   `user_id` INT NOT NULL AUTO_INCREMENT ,
  9.   `user_name` TINYBLOB NOT NULL ,
  10.   PRIMARY KEY (`user_id`) ,
  11.   UNIQUE INDEX `user_name_UNIQUE` (`user_name`(255) ASC) )
  12. ENGINE = InnoDB;
  13.  
  14.  
  15. -- -----------------------------------------------------
  16. -- Table `mydb`.`permissions`
  17. -- -----------------------------------------------------
  18. CREATE  TABLE IF NOT EXISTS `mydb`.`permissions` (
  19.   `permission_id` INT NOT NULL AUTO_INCREMENT ,
  20.   `permission_name` TINYBLOB NULL DEFAULT NULL ,
  21.   PRIMARY KEY (`permission_id`) ,
  22.   UNIQUE INDEX `permission_name_UNIQUE` (`permission_name`(255) ASC) )
  23. ENGINE = InnoDB;
  24.  
  25.  
  26. -- -----------------------------------------------------
  27. -- Table `mydb`.`users_permissions`
  28. -- -----------------------------------------------------
  29. CREATE TABLE `users_permissions` (
  30.   `user_id` INT(11) NOT NULL,
  31.   `permission_id` INT(11) NOT NULL,
  32.   PRIMARY KEY (`user_id`,`permission_id`),
  33.   KEY `user_id` (`user_id`),
  34.   KEY `permission_id` (`permission_id`),
  35.   KEY `users_permissions_perm` (`permission_id`),
  36.   KEY `users_permissions_user` (`user_id`),
  37.   CONSTRAINT `users_permissions_permissions`
  38.     FOREIGN KEY (`permission_id`)
  39.     REFERENCES `permissions` (`permission_id`)
  40.     ON DELETE CASCADE
  41.     ON UPDATE NO ACTION,
  42.   CONSTRAINT `users_permissions_users`
  43.     FOREIGN KEY (`user_id`)
  44.     REFERENCES `users` (`user_id`)
  45.     ON DELETE CASCADE
  46.     ON UPDATE NO ACTION
  47. ) 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.

  1. class User {
  2.     //I'm assuming here that you already have a User class, once again I will
  3.     //only give here code that applies to the ACL
  4.  
  5.     protected $id;
  6.     protected $username;
  7.     /**
  8.      * A list of permissions for this user. Will be filled
  9.      * by the first call to the ::can() method.
  10.      */
  11.     protected $permissions;
  12.  
  13.     /* ... */
  14.  
  15.     /**
  16.      * returns true if the user represented by the object can do the action 
  17.      * given as a param.
  18.      * @param permission String holding a permission name
  19.      * @return boolean, true if the user can, false if he can't
  20.      */
  21.     public function can( $permission ) {
  22.         if ( ! isset( $this->permissions ) ) {
  23.             /*
  24.              fetch permissions for this user from the database, and set the 
  25.              User::permissions property, to allow caching.
  26.              */
  27.             $perm_stmt = DBSlave::getInstance()
  28.                 ->prepare( 'SELECT permissions.permission_name FROM permissions, users_permissions 
  29.                             WHERE users_permissions.user_id = :userid 
  30.                                 AND users_permissions.permission_id = permissions.permission_id' );
  31.             $perm_stmt->execute( array( ':userid' => $this->id ) );
  32.             $this->permissions = $perm_stmt->fetchAll( PDO::FETCH_COLUMN, 0 );
  33.         }
  34.         return in_array( $permission, $this->permissions );
  35.     }
  36. }
  37.  
  38. //instantiate a user
  39. $that_guy = new User();
  40.  
  41. if ( $that_guy->can( 'edit' ) ) {
  42.     //do what has to be done, write to the database,
  43.     //display the page, etc...
  44. }

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 :

  1. class User {
  2.     protected $id;
  3.     /**
  4.      * A list of permissions for this user. Will be filled
  5.      * by the first call to the ::can() method.
  6.      */
  7.     protected $permissions;
  8.  
  9.     /**
  10.      * returns true if the user represented by the object can do the action(s)
  11.      * given as a param.
  12.      * @param permission mixed a string holding a single permission name, or
  13.             an array of strings, each holding a permission name
  14.      * @param and_or bool if true all permissions must be granted to the user
  15.             if false, any of them is sufficient
  16.      * @return boolean true if the user can, false if he can't
  17.      */
  18.     public function can( $permission, $and_or = true ) {
  19.         /* 
  20.           if an empty string or array is given, it may well be that an
  21.           abstraction method was used to check for permission, and we're in the
  22.           case where no permission is required to perform the subsequent action
  23.          */
  24.         if ( empty( $permission ) ) return true;
  25.  
  26.         /* if $this->permissions isn't set, this method has not been called yet
  27.           for that script */
  28.         if ( ! isset( $this->permissions ) ) {
  29.             /*
  30.              fetch permissions for this user from the database, and set the
  31.              User::permissions property, to allow caching.
  32.              User::permissions will be an array with permission names as keys,
  33.              and 1s as values.
  34.              */
  35.             $perm_stmt = DBSlave::getInstance()
  36.                 ->prepare( 'SELECT permissions.permission_name
  37.                             FROM permissions, users_permissions
  38.                             WHERE users_permissions.user_id = :userid
  39.                                 AND users_permissions.permission_id = permissions.permission_id' );
  40.             $perm_stmt->execute( array( ':userid' => $this->id ) );
  41.             $this->permissions = array_fill_keys( $perm_stmt->fetchAll(PDO::FETCH_COLUMN,0), 1 );
  42.         }
  43.  
  44.         /*
  45.          If a single permission is requested, as a string, just check whether
  46.          or not it's a key of the User::permissions array
  47.          All keys in User::permissions have been granted to the user, and have
  48.          1 as an attached value
  49.          All ungranted permissions do not appear as keys of User::permissions
  50.          */
  51.         if ( is_string( $permission ) ) {
  52.             return array_key_exists( $permission, $this->permissions );
  53.         }
  54.  
  55.         /*
  56.          If not, we'll have to check for all of them, and AND/OR them,
  57.          depending on the value of the second, optional parameter.
  58.          First, we'll need an array with requested permissions as keys, and 0s
  59.          as values, in which we will set 1s for every granted permission.
  60.          Easy way to do so is to use array_intersect_key, getting the value of
  61.          each key of $this->permissions that is present in $permission, then
  62.          simply adding an array_fill_keys version of $permission.
  63.          */
  64.         $checked_permissions = array_intersect_key( $this->permissions,
  65.                                                     array_fill_keys( $permission, 0 )
  66.                                                     ) + array_fill_keys($permission,0);
  67.  
  68.         /*
  69.           Now we will need array_sum if any of the requested permissions is
  70.           sufficient (giving a non-null sum if one of them at least is granted)
  71.           or array_product if all of them are needed (giving a null product if
  72.           one of them at least isn't granted)
  73.           */
  74.         return $and_or ? (bool) array_product( $checked_permissions ) : (bool) array_sum( $checked_permissions );
  75.     }
  76. }
  77.  
  78. $that_guy = new User();
  79.  
  80. /*
  81.   And, assuming $that_guy->permissions to be array('foo'=>1,'bar'=>1)
  82.   once $that_guy->can() is first called :
  83.  */
  84. //available permissions
  85. var_dump( $that_guy->can( 'foo' ) );//true
  86. var_dump( $that_guy->can( 'bar' ) );//true
  87.  
  88. //unavailable permission
  89. var_dump( $that_guy->can( 'quok' ));//false
  90.  
  91. //two available permissions
  92. var_dump( $that_guy->can( array( 'foo', 'bar' ) ) );//true
  93.  
  94. //one available, one unavailable, when requesting for both
  95. //these two lines are exactly identical in functionality
  96. var_dump( $that_guy->can( array( 'foo', 'quok' ) ) );//false
  97. var_dump( $that_guy->can( array( 'foo', 'quok' ), true ) );//false, like previous line
  98.  
  99. //one available, one unavailable, when requesting for at least one
  100. var_dump( $that_guy->can( array( 'foo', 'quok' ), false ) );//true, 'foo' is sufficient

If you feel like something’s missing, it’s probably because this class doesn’t have a method to log unauthorized access attempts yet. Here goes :

  1. class User {
  2.     /**
  3.      * Logs unauthorized attempts at accessing a permission-protected by the
  4.      * current user
  5.      * @param perm mixed something representing a permission
  6.      */
  7.     public function logUnauthorizedAccess( $perm ) {
  8.         //normalization
  9.         if ( is_object( $perm ) && ! is_a( $perm, 'Permission' ) ) {
  10.             return;
  11.         }
  12.         elseif ( ( is_string( $perm ) || is_int( $perm ) ) && $perm = new Permission( $perm ) && $perm->getId() ) {
  13.             return;
  14.         }
  15.  
  16.         /* You can now log the actual wrongful access attempt. */
  17.     }
  18. }

You can of course integrate it with the User::can() method.

There are many ways to log those attempts, each with its own advantages :

  • error_log() is built-in, but limited to the error_log file currently in use by PHP. It has the advantage of being built-in, and of being parsed and handled accordingly by log parsers ;
  • any other way to write to a flat file, for example file_put_contents() ;
  • 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) :

  1. /**
  2.  * A bogus class representing a user.
  3.  * All methods listed here pertain to and only to the access control system
  4.  */
  5. class User {
  6.     protected $id;
  7.     protected $username;
  8.  
  9.     /**
  10.      * An id => name array of permissions currently granted to the user
  11.      */
  12.     protected $permissions;
  13.  
  14.     public function __construct( $user ) {
  15.         if ( ( is_int( $user ) || ctype_digit( $user ) ) && $user > 0 ) {
  16.             if ( $username = DBSlave::getInstance()
  17.                         ->query( "SELECT user_name FROM users WHERE user_id = $user")
  18.                         ->fetch( PDO::FETCH_COLUMN, 0 ) ) {
  19.                 $this->id = $user;
  20.                 $this->username = $username;
  21.             }
  22.             else return;
  23.         }
  24.         else return;
  25.     }
  26.  
  27.     /**
  28.      * returns true if the user represented by the object can do the action(s)
  29.      * given as a param.
  30.      * @param permission mixed a string holding a single permission name, or
  31.             an array of strings, each holding a permission name
  32.      * @param and_or bool if true all permissions must be granted to the user
  33.             if false, any of them is sufficient
  34.      * @return boolean true if the user can, false if he can't
  35.      */
  36.     public function can( $permission, $and_or = true ) {
  37.         /*
  38.           if an empty string or array is given, it may well be that an
  39.           abstraction method was used to check for permission, and we're in the
  40.           case where no permission is required to perform the subsequent action
  41.           */
  42.         if ( empty( $permission ) ) {
  43.             return true;
  44.         }
  45.  
  46.         /* if $this->permissions isn't set, this method has not been called yet
  47.           for that script */
  48.         if ( empty( $this->permissions ) ) {
  49.             $this->fetchPerms();
  50.         }
  51.  
  52.         /*
  53.           If a single permission is requested, as a string, just check whether
  54.           or not it's a key of the User::permissions array
  55.           All keys in User::permissions have been granted to the user, and have
  56.           1 as an attached value
  57.           All ungranted permissions do not appear as keys of User::permissions
  58.           */
  59.         if ( is_string( $permission ) ) {
  60.             return in_array( $permission, $this->permissions );
  61.         }
  62.  
  63.         /*
  64.           If not, we'll have to check for all of them, and AND/OR them,
  65.           depending on the value of the second parameter.
  66.           First, we'll need an array with requested permissions as keys, and 0s
  67.           as values, in which we will set 1s for every granted permission
  68.           Easy way to do so is to use array_intersect_key, getting the value of
  69.           each key of User::$permissions that is present in $permission, then
  70.           simply adding an array_fill_keys version of $permission
  71.           */
  72.         $checked_permissions = array_intersect_key(
  73.                                     array_fill_keys( $this->permissions, 1 ),
  74.                                     array_fill_keys( $permission, 1 )
  75.                         ) + array_fill_keys( $permission, 0 );
  76.  
  77.         /*
  78.           Now we will need array_sum if any of the requested permissions is
  79.           sufficient (giving a non-null sum if one of them at least is granted)
  80.           or array_product if all of them are needed (giving a null product if
  81.           one of them at least is null)
  82.           */
  83.         return $and_or ? (bool) array_product( $checked_permissions ) : (bool) array_sum( $checked_permissions );
  84.     }
  85.  
  86.     /**
  87.      * Fetch permissions for this user from the database, and set the
  88.      * User::$permissions property, to allow caching.
  89.      * User::$permissions will be an array with permission names as keys.
  90.      * @return bool true on success, false on failure
  91.      */
  92.     private function fetchPerms() {
  93.         $this->permissions = DBSlave::getInstance()
  94.                         ->query( "SELECT users_permissions.permission_id, permissions.permission_name
  95.                                     FROM permissions, users_permissions
  96.                                     WHERE users_permissions.user_id = {$this->id}
  97.                                         AND users_permissions.permission_id = permissions.permission_id" )
  98.                         ->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE );
  99.         return is_array( $this->permissions );
  100.     }
  101.  
  102.     /**
  103.      * Essentially a god mode, gives all currently extant permissions to the
  104.      * current user.
  105.      * @return bool true on success, false on failure
  106.      */
  107.     public function grantAllPerms() {
  108.         $allPerms = DBSlave::getInstance()
  109.                         ->query( 'SELECT permission_id FROM permissions' )->fetchAll(PDO::FETCH_COLUMN, 0);
  110.         $grantPerm = DBMaster::getInstance()
  111.                         ->prepare( 'INSERT INTO users_permissions (user_id, permission_id)
  112.                                     VALUES (:userid, :permid )
  113.                                     ON DUPLICATE KEY UPDATE permission_id = permission_id');
  114.         $grantPerm->bindValue( ':userid', $this->id );
  115.         $grantPerm->bindParam( ':permid', $perm );
  116.         foreach ( $allPerms as $perm ) {
  117.             if ( ! $grantPerm->execute() ) {
  118.                 return false;
  119.             }
  120.         }
  121.         $this->fetchPerms();
  122.         return true;
  123.     }
  124.  
  125.     /**
  126.      * Grants the current user with a given permission
  127.      * @param perm mixed a permission represented by name, id, or object
  128.      * @return mixed true on success, false on failure
  129.      */
  130.     public function grantPerm( $perm ) {
  131.         if ( ! $perm = Permission::normalizeToID( $perm ) ) {
  132.             return false;
  133.         }
  134.         return DBMaster::getInstance()
  135.                         ->prepare( 'INSERT INTO users_permissions (user_id, permission_id)
  136.                                     VALUES (:userid, :permid)' )
  137.                         ->execute( array( ':userid' => $this->id, ':permid' => $perm ) );
  138.     }
  139.  
  140.     /**
  141.      * Lists permissions granted to the current user
  142.      * @refresh bool true to refresh the permission cache for this user
  143.      * @return array an id=>name array of all permissions of the current user
  144.      */
  145.     public function listPerms( $refresh = false ) {
  146.         if ( empty( $this->permissions ) || $refresh ) {
  147.             $this->fetchPerms();
  148.         }
  149.         return $this->permissions;
  150.     }
  151.  
  152.     /**
  153.      * Sets multiple permissions for the current user.
  154.      * Can effectively grant or ungrant permissions, but is more efficient
  155.      * when setting multiple permissions than User::grantPerm() and
  156.      * User::ungrantPerm()
  157.      * @param perms array an array of permissions names, ids, or objects
  158.      * @return bool true on success, false on failure
  159.      */
  160.     public function setPerms ( $setperms = null ) {
  161.         if ( ! is_array( $setperms ) ) {
  162.             return false;
  163.         }
  164.         if ( empty( $setperms ) ) {
  165.             return $this->ungrantAllPerms();
  166.         }
  167.         $this->fetchPerms();
  168.         $setperms = array_filter( array_map( array( 'Permission', 'normalizeToID' ), $setperms ) );
  169.         $allperms = array_keys( PermissionsList::listAllPerms() );
  170.         $adduserperm = array();
  171.         $deluserperm = array();
  172.         foreach ( $allperms as $perm ) {
  173.             if ( ! in_array( $perm, array_keys( $this->permissions ) ) && in_array( $perm, $setperms ) ) {
  174.                 $adduserperm[] = $perm;
  175.             }
  176.         }
  177.         foreach ( array_keys( $this->permissions ) as $perm ) {
  178.             if ( ! in_array( $perm, $setperms ) ) {
  179.                 $deluserperm[] = $perm;
  180.             }
  181.         }
  182.         if ( ! empty( $adduserperm ) ) {
  183.             $permaddstmt = DBMaster::getInstance()
  184.                             ->prepare( 'INSERT INTO users_permissions
  185.                                         VALUES ( :userid, :permid )' );
  186.             $permaddstmt->bindValue( ':userid', $this->id );
  187.             $permaddstmt->bindParam( ':permid', $addperm );
  188.             foreach ( $adduserperm as $addperm ) {
  189.                 $permaddstmt->execute();
  190.             }
  191.         }
  192.         if ( ! empty( $deluserperm ) ) {
  193.             $permdelstmt = DBMaster::getInstance()
  194.                             ->prepare( 'DELETE FROM users_permissions
  195.                                         WHERE user_id = :userid
  196.                                             AND permission_id = :permid' );
  197.             $permdelstmt->bindValue( ':userid', $this->id );
  198.             $permdelstmt->bindParam( ':permid', $delperm );
  199.             foreach ( $deluserperm as $delperm ) {
  200.                 $permdelstmt->execute();
  201.             }
  202.         }
  203.         $this->fetchPerms();
  204.     }
  205.  
  206.     /**
  207.      * Essentially a ban, removes all permissions the current user is currently
  208.      * granted with
  209.      * @return bool true on success, false on failure
  210.      */
  211.     public function ungrantAllPerms() {
  212.         $this->permissions = array();
  213.         return DBMaster::getInstance()
  214.                     ->prepare( 'DELETE FROM users_permissions
  215.                                 WHERE user_id = :userid' )
  216.                     ->execute( array( ':userid' => $this->id ) );
  217.     }
  218.  
  219.     /**
  220.      * Ungrants the current user a given permission
  221.      * @param perm mixed a permission represented by name, id, or object
  222.      * @return mixed true on success, false on failure
  223.      */
  224.     public function ungrantPerm( $perm ) {
  225.         if ( ! $perm = Permission::normalizeToID( $perm ) ) {
  226.             return false;
  227.         }
  228.         return DBMaster::getInstance()
  229.                         ->prepare( 'DELETE FROM users_permissions
  230.                                     WHERE user_id = :userid
  231.                                         AND permission_id = :permid' )
  232.                         ->execute( array( ':userid' => $this->id, ':permid' => $perm ) );
  233.     }
  234. }

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.

This requires a bit more code. Let’s write code to add a permission :

DBMaster::getInstance()
    ->prepare( 'INSERT INTO permissions (permission_id, permission_name)
                VALUES (NULL, :newperm)' )
    ->execute( array( ':newperm' => $newPermName ) );

And to delete a permission :

/*
  The prepared statement I've chosen requires an integer permission id,
  adapt your form accordingly
 */
DBMaster::getInstance()
        ->prepare( 'DELETE FROM permissions 
            WHERE permission_id = :deleteperm' )
        ->execute( array( ':deleteperm' => $deletePermId ) );

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.

To grant one :

/*
 Requires integers, for user id and permission id
 */
DBMaster::getInstance()
        ->prepare( 'INSERT INTO users_permissions (user_id, permission_id) 
                    VALUES (:userid, :permid)' )
        ->execute( array( ':userid' => $userid, ':permid' => $permid ) );

To ungrant one :

/*
 Requires integers, for user id and permission id
 */
DBMaster::getInstance()
        ->prepare( 'DELETE FROM users_permissions
                    WHERE user_id = :userid
                        AND permission_id = :permid)' )
        ->execute( array( ':userid' => $userid, ':permid' => $permid ) );

And one could go on, and on, and on…
What did you say ? Those could be merged in a class, OOP-style ? Why, certainly !

  1. /**
  2.  * The permission class itself
  3.  * An instance of this class refers to a currently existing permission
  4.  */
  5. class Permission {
  6.     protected $id;
  7.     protected $name;
  8.     protected $userlist;
  9.  
  10.     /**
  11.      * Usual constructor. Allows several kinds of param, triggers other
  12.      * methods accordingly
  13.      * If the Permission is referred to by id, and no permission by that id
  14.      * exist, the Permission instance will essentially be empty
  15.      * If the Permission is referred to by name, and no permission by that name
  16.      * exists, will attempt to create it and set $this->name and $this->id,
  17.      * by calling Permission::create()
  18.      * @param perm int|string something representing the permission, either
  19.             its id or its name
  20.      */
  21.     public function __construct( $perm ) {
  22.         if ( ( is_int( $perm ) || ctype_digit( $perm ) ) && $perm > 0 ) {
  23.             return $this->fetchById( $perm );
  24.         }
  25.         if ( is_string( $perm ) ) {
  26.             return $this->fetchByName( $perm );
  27.         }
  28.     }
  29.  
  30.     /**
  31.      * Fallback function to create permissions on the go. Will be called by the
  32.      * constructor (or more accurately Permission::fetchByName()) when provided
  33.      * with an unrecognized permission name.
  34.      * Once the permission is in the DB, will set $this->id and $this->name
  35.      * to conclude __construct functionality
  36.      * @param name string the new permission name
  37.      */
  38.     private function create( $name ) {
  39.         if ( ( DBMaster::getInstance()
  40.                     ->prepare( 'INSERT INTO permissions (permission_id, permission_name)
  41.                                 VALUES (NULL, :newperm)' )
  42.                     ->execute( array( 'newperm' => $name ) ) )
  43.                 && ( $id = DBMaster::getInstance()->lastInsertId() ) ) {
  44.             $this->name = $name;
  45.             $this->id = $id;
  46.             return true;
  47.         }
  48.         else {
  49.             unset( $this->name );
  50.             return false;
  51.         }
  52.     }
  53.  
  54.     /**
  55.      * Deletes all records regarding the current permission in the database,
  56.      * including all links to the users in the users_permissions table.
  57.      * @return bool true on success, false on failure (database not writable)
  58.      */
  59.     public function delete() {
  60.         if ( ! isset( $this->id ) ) {
  61.             return true;
  62.         }
  63.         $a = DBMaster::getInstance()
  64.                     ->prepare( 'DELETE FROM permissions
  65.                         WHERE permission_id = :deleteperm' )
  66.                     ->execute( array( ':deleteperm' => $this->id ) );
  67.         unset( $this->id, $this->name );
  68.         return $a;
  69.     }
  70.  
  71.     /**
  72.      * Fetches a permission using its permission id
  73.      * If a permission with this id doesn't exist, will return false
  74.      * @param id int
  75.      * @return mixed an object representing the permission, or false on failure
  76.      */
  77.     private function fetchById( $id ) {
  78.         $this->id = $id;
  79.         $namestatement = DBSlave::getInstance()
  80.                     ->prepare( 'SELECT permission_name
  81.                                 FROM permissions
  82.                                 WHERE permission_id = :permid' );
  83.         $namestatement->execute( array( ':permid' => $id ) );
  84.         if ( $namerow = $namestatement->fetch() ) {
  85.             $this->name = $namerow['permission_name'];
  86.             return true;
  87.         }
  88.         unset( $this->id, $this->name );
  89.         return false;
  90.     }
  91.  
  92.     /**
  93.      * Fetches a permission using its permission name
  94.      * If a permission by this name doesn't exist, will attempt to create one
  95.      * @param name string the name of the permission
  96.      * @return object an instance of Permission representing the permission by
  97.             that name
  98.      */
  99.     private function fetchByName( $name ) {
  100.         $this->name = $name;
  101.         $namestatement = DBSlave::getInstance()
  102.                     ->prepare( 'SELECT permission_id
  103.                                 FROM permissions
  104.                                 WHERE permission_name = :permname' );
  105.         $namestatement->execute( array( ':permname' => $name ) );
  106.         if ( $namerow = $namestatement->fetch() ) {
  107.             $this->id = $namerow['permission_id'];
  108.             return true;
  109.         }
  110.         return $this->create( $name );
  111.     }
  112.  
  113.     /**
  114.      * Gets the id of the permission
  115.      * @return int the id of the permission
  116.      */
  117.     public function getId() {
  118.         if ( ! isset( $this->id ) ) {
  119.             return false;
  120.         }
  121.         return $this->id;
  122.     }
  123.  
  124.     /**
  125.      * Gets the name of the permission
  126.      * @return string the name of the permission
  127.      */
  128.     public function getName() {
  129.         if ( ! isset( $this->name ) ) {
  130.             return false;
  131.         }
  132.         return $this->name;
  133.     }
  134.  
  135.     /**
  136.      * Lists all users who currently are granted that permission
  137.      * @param refresh bool whether or not to refresh the cache for the list
  138.             of users having that permission
  139.      * @return array an array with user ids as keys, usernames as values
  140.      */
  141.     public function listUsers( $refresh = false ) {
  142.         if ( ! isset( $this->id ) ) {
  143.             return array();
  144.         }
  145.         if ( ! isset( $this->userList ) || $refresh ) {
  146.             $userlist = DBMaster::getInstance()
  147.                                 ->prepare( 'SELECT users_permissions.user_id, users.user_name
  148.                                             FROM users, users_permissions
  149.                                             WHERE users_permissions.permission_id = :permid
  150.                                                 AND users.user_id = users_permissions.user_id' );
  151.             $userlist->execute( array( ':permid' => $this->id ) );
  152.             $this->userList = $userlist->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE );
  153.         }
  154.         return $this->userList;
  155.     }
  156.  
  157.     /**
  158.      * Normalizes a permission represented by its name, id, or object
  159.      * and returns its id.
  160.      * @param mixed perm a permission represented by its name, id, or object
  161.      * @return mixed a permission id, or false on failure
  162.      */
  163.     public static function normalizeToID( $perm ) {
  164.         if ( is_object( $perm ) && is_a( $perm, 'Permission' ) ) {
  165.             return $perm->getId();
  166.         }
  167.         if ( is_string( $perm ) || is_int( $perm ) ) {
  168.             $perm = new Permission( $perm );
  169.             return $perm->getId();
  170.         }
  171.         return false;
  172.     }
  173.  
  174.     /**
  175.      * Normalizes a permission represented by its name, id, or object
  176.      * and returns its name
  177.      * @param mixed perm a permission represented by its name, id, or object
  178.      * @return mixed a permission name, or false on failure
  179.      */
  180.     public static function normalizeToName( $perm ) {
  181.         if ( is_object( $perm ) && is_a( $perm, 'Permission' ) ) {
  182.             return $perm->getName();
  183.         }
  184.         if ( is_string( $perm ) || is_int( $perm ) ) {
  185.             $perm = new Permission( $perm );
  186.             return $perm->getName();
  187.         }
  188.         return false;
  189.     }
  190.  
  191.     /**
  192.      * Renames a permission in the database
  193.      * @param newname string the new name of the permission
  194.      * @return bool true on success, false on failure
  195.      */
  196.     public function rename( $newname ) {
  197.         if ( ! isset( $this->id ) ) {
  198.             return true;
  199.         }
  200.         return DBMaster::getInstance()
  201.                     ->prepare( 'UPDATE permissions
  202.                                 SET permission_name = :newname
  203.                                 WHERE permission_id = :permid' )
  204.                     ->execute( array( ':newname' => $newname, ':permid' => $this->id ) );
  205.     }
  206. }

You can obviously add exceptions in the mix.

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

  1. class PermissionsList {
  2.     protected static $allPerms;
  3.  
  4.     private function __construct() { }
  5.  
  6.     /**
  7.      * Returns a list of all permissions currently in the permissions
  8.      * table
  9.      * @param refresh bool whether or not to refresh the list of permissions.
  10.      * @return array an array, each element of which represents a single
  11.      *     permission with its id as key and name as value
  12.      */
  13.     public static function listAllPerms( $refresh = false ) {
  14.         if ( ! isset( self::$allPerms ) || $refresh ) {
  15.             self::$allPerms = DBSlave::getInstance()
  16.                         ->query( 'SELECT permission_id, permission_name
  17.                                   FROM permissions')
  18.                         ->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE );
  19.         }
  20.         return self::$allPerms;
  21.     }
  22. }

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

  1. <?php
  2. //create a User instance for the user currently browsing the script.
  3. $currentUser = User::getCurrent();
  4.  
  5. //check if the current user has a view permission
  6. if ( ! $currentUser->can( 'view' ) ) {
  7.     header( 'Location: error.php?perm=view' );
  8.     return;
  9. }
  10. /* The rest of the code goes here */

IVB. Permissions manager

  1. <?php
  2. //create a User instance for the user currently browsing the script.
  3. $currentUser = User::getCurrent();
  4.  
  5. //check if the current user can edit permissions
  6. if ( !$currentUser->can( 'giveperms' ) ) {
  7.     header( 'Location: error.php?perm=giveperms' );
  8.     return;
  9. }
  10.  
  11. if ( empty( $_GET['permid'] ) && empty( $_POST['newperm'] ) ) {
  12.     //let's delete that permission on a POST request
  13.     if ( !empty( $_POST['deleteperm'] ) ) {
  14.         $deleteperm = new Permission( $_POST['deleteperm'] );
  15.         if ( $deleteperm->getId() ) {
  16.             $message = 'The permission you requested for deletion (' . $deleteperm->getName() . ') has been successfully deleted.';
  17.             $deleteperm->delete();
  18.         }
  19.         else {
  20.             $message = 'The permission you tried to delete doesn\'t exist';
  21.         }
  22.     }
  23.     /* Display a list all current permissions here */
  24.     $permList = PermissionsList::listAllPerms();
  25.     ?>
  26. <html>
  27.     <head>
  28.         <title>Choose a permission or create a new one</title>
  29.     </head>
  30.     <body>
  31.     <?php 
  32.     if ( !empty( $message ) ) {
  33.     ?>
  34.         <div><?php echo $message; ?></div>
  35.     <?php
  36.     }
  37.     ?>
  38.         <ul>
  39.     <?php
  40.     foreach ( $permList as $permid => $permname ) {
  41.         ?>
  42.             <li><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>?permid=<?php echo $permid; ?>">(<?php echo $permid; ?>) <?php echo $permname; ?></a></li>
  43.         <?php
  44.     }
  45.     ?>
  46.         </ul>
  47.         <form action="" method="post">
  48.             <label for="newperm">Name of the new permission :</label> <input type="text" name="newperm" />
  49.             <input type="submit" />
  50.         </form>
  51.         </form>
  52.     </body>
  53. </html>
  54.     <?php
  55.     return;
  56. }
  57.  
  58. //create an instance of the Permission class, be it a pre-existing permission
  59. //or a new one
  60. if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
  61.     $perm = $_POST['newperm'];
  62. }
  63. else {
  64.     $perm = $_GET['permid'];
  65. }
  66. //this line will find a permission with the requested id (in the case of an
  67. //existing permission), or attempt to create a new one
  68. $permission = new Permission( $perm );
  69.  
  70. if ( ! $permission->getId() ) {
  71.     echo 'This permission doesn\'t exist';
  72.     return;
  73. }
  74.  
  75. //fetch a refreshed list of all users with the current permission in the DB
  76. $allUsers = $permission->listUsers(true);
  77.  
  78. ?>
  79. <html>
  80.     <head>
  81.         <title>Users with the <?php echo htmlentities( $permission->getName() ); ?> permission</title>
  82.     </head>
  83.     <body>
  84.         <ul>
  85. <?php
  86. foreach ( $allUsers as $userid => $username ) {
  87.     ?>
  88.             <li><a href="manageuserperms.php?userid=<?php echo $userid; ?>">(<?php echo $userid; ?>) <?php echo htmlentities( $username ); ?></a></li>
  89.     <?php
  90. }
  91. ?>
  92.         </ul>
  93.         <form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
  94.             <input type="hidden" name="deleteperm" value="<?php echo $permission->getId(); ?>" />
  95.             <input type="submit" value="delete this permission" />
  96.         </form>
  97.     </body>
  98. </html>
  99. <?php
  100. return;

The code for manageuserperms.php follows.

IVC. Users’ permissions manager

And now for the big stuff : a permissions manager.
The one I’m about to disclose is User-based, but you could create a Permission-based one in a rather similar fashion.

  1. <?php
  2. //create a User instance for the user currently browsing the script.
  3. $currentUser = User::getCurrent();
  4.  
  5. //check if the current user can grant permissions
  6. if ( ! $currentUser->can( 'giveperms' ) ) {
  7.     header( 'Location: error.php?perm=giveperms' );
  8.     return;
  9. }
  10.  
  11. if ( empty( $_GET['userid'] ) ) {
  12.     /* Display a list all current users here */
  13.     $userList = DBSlave::getInstance()
  14.                 ->query( "SELECT user_id, user_name
  15.                             FROM users" )
  16.                 ->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_UNIQUE | PDO::FETCH_GROUP );
  17.     ?>
  18. <html>
  19.     <head>
  20.         <title>Choose a user to give permissions to</title>
  21.     </head>
  22.     <body>
  23.         <ul>
  24.     <?php
  25.     foreach ( $userList as $userid => $username ) {
  26.         ?>
  27.             <li><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>?userid=<?php echo $userid; ?>">(<?php echo $userid; ?>) <?php echo $username; ?></a></li>
  28.         <?php
  29.     }
  30.     ?>
  31.         </ul>
  32.     </body>
  33. </html>
  34.     <?php
  35.     return;
  36. }
  37.  
  38. //create a User instance for the user whose permissions the current user
  39. //is about to edit
  40. $user = new User( $_GET['userid'] );
  41.  
  42. if ( $_SERVER['REQUEST_METHOD'] == 'POST' && ! empty( $_GET['userid'] ) ) {
  43.     //update the database accordingly
  44.     if ( empty( $_POST['perms'] ) || ! is_array( $_POST['perms'] ) ) {
  45.         $_POST['perms'] = array();
  46.     }
  47.     $user->setPerms( $_POST['perms'] );
  48. }
  49.  
  50. //fetch a list of all existing permissions in the database
  51. $allPerms = PermissionsList::listAllPerms();
  52.  
  53. //refresh the permissions for the target user
  54. $userPerms = $user->listPerms(true);
  55.  
  56. ?>
  57. <html>
  58.     <head>
  59.         <title>Permissions for <?php echo htmlentities( $user->getName() ); ?></title>
  60.     </head>
  61.     <body>
  62.         <form action="" method="post">
  63.             <ul>
  64. <?php
  65. foreach ( $allPerms as $permid => $permname ) {
  66.     $checked = in_array( $permname, $userPerms ) ? ' checked="checked"' : '';
  67.     ?>
  68.                 <li><input type="checkbox"<?php echo $checked; ?> name="perms[]" id="perm<?php echo $permid; ?>" value="<?php echo $permid; ?>" />
  69.                 <label for="perm<?php echo $permid; ?>"><?php echo htmlentities( $permname ); ?></label></li>
  70.     <?php
  71. }
  72. ?>
  73.             </ul>
  74.             <input type="submit" />
  75.         </form>
  76.     </body>
  77. </html>
  78. <?php
  79. return;

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.

This blogpost is provided under a
Creative Commons, Attribution-NonCommercial-ShareAlike license.
Feel free to redistribute or translate it under the terms of the license.
Code in this blogpost (and only code) is provided under a warranty-free
WTF-PL v2 license.
Feel free to comply with that very simple, one-clause, non-viral license 🙂

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">