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
II. User class
III. CRUD. The Permission class
IV. Usage
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 IF NOT EXISTS `mydb` ;
USE `mydb` ;
-- -----------------------------------------------------
-- Table `mydb`.`users`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `mydb`.`users` (
`user_id` INT NOT NULL AUTO_INCREMENT ,
`user_name` TINYBLOB NOT NULL ,
PRIMARY KEY (`user_id`) ,
UNIQUE INDEX `user_name_UNIQUE` (`user_name`(255) ASC) )
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `mydb`.`permissions`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `mydb`.`permissions` (
`permission_id` INT NOT NULL AUTO_INCREMENT ,
`permission_name` TINYBLOB NULL DEFAULT NULL ,
PRIMARY KEY (`permission_id`) ,
UNIQUE INDEX `permission_name_UNIQUE` (`permission_name`(255) ASC) )
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `mydb`.`users_permissions`
-- -----------------------------------------------------
CREATE TABLE `users_permissions` (
`user_id` INT(11) NOT NULL,
`permission_id` INT(11) NOT NULL,
PRIMARY KEY (`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`
FOREIGN KEY (`permission_id`)
REFERENCES `permissions` (`permission_id`)
ON DELETE CASCADE
ON UPDATE NO ACTION,
CONSTRAINT `users_permissions_users`
FOREIGN KEY (`user_id`)
REFERENCES `users` (`user_id`)
ON DELETE CASCADE
ON UPDATE 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
* given as a param.
* @param permission String holding a permission name
* @return boolean, true if the user can, false if he can't
*/
public function can( $permission ) {
if ( ! isset( $this->permissions ) ) {
/*
fetch permissions for this user from the database, and set the
User::permissions property, to allow caching.
*/
$perm_stmt = DBSlave::getInstance()
->prepare( 'SELECT permissions.permission_name FROM permissions, users_permissions
WHERE users_permissions.user_id = :userid
AND users_permissions.permission_id = permissions.permission_id' );
$perm_stmt->execute( array( ':userid' => $this->id ) );
$this->permissions = $perm_stmt->fetchAll( PDO::FETCH_COLUMN, 0 );
}
return in_array( $permission, $this->permissions );
}
}
//instantiate a user
$that_guy = new User();
if ( $that_guy->can( 'edit' ) ) {
//do what has to be done, write to the database,
//display the page, etc...
}
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
*/
public function 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 ) ) return true;
/* if $this->permissions isn't set, this method has not been called yet
for that script */
if ( ! isset( $this->permissions ) ) {
/*
fetch permissions for this user from the database, and set the
User::permissions property, to allow caching.
User::permissions will be an array with permission names as keys,
and 1s as values.
*/
$perm_stmt = DBSlave::getInstance()
->prepare( 'SELECT permissions.permission_name
FROM permissions, users_permissions
WHERE users_permissions.user_id = :userid
AND users_permissions.permission_id = permissions.permission_id' );
$perm_stmt->execute( array( ':userid' => $this->id ) );
$this->permissions = array_fill_keys( $perm_stmt->fetchAll(PDO::FETCH_COLUMN,0), 1 );
}
/*
If a single permission is requested, as a string, just check whether
or not it's a key of the User::permissions array
All keys in User::permissions have been granted to the user, and have
1 as an attached value
All ungranted permissions do not appear as keys of User::permissions
*/
if ( is_string( $permission ) ) {
return array_key_exists( $permission, $this->permissions );
}
/*
If not, we'll have to check for all of them, and AND/OR them,
depending on the value of the second, optional parameter.
First, we'll need an array with requested permissions as keys, and 0s
as values, in which we will set 1s for every granted permission.
Easy way to do so is to use array_intersect_key, getting the value of
each key of $this->permissions that is present in $permission, then
simply adding an array_fill_keys version of $permission.
*/
$checked_permissions = array_intersect_key( $this->permissions,
array_fill_keys( $permission, 0 )
) + array_fill_keys($permission,0);
/*
Now we will need array_sum if any of the requested permissions is
sufficient (giving a non-null sum if one of them at least is granted)
or array_product if all of them are needed (giving a null product if
one of them at least isn't granted)
*/
return $and_or ? (bool) array_product( $checked_permissions ) : (bool) array_sum( $checked_permissions );
}
}
$that_guy = new User();
/*
And, assuming $that_guy->permissions to be array('foo'=>1,'bar'=>1)
once $that_guy->can() is first called :
*/
//available permissions
var_dump( $that_guy->can( 'foo' ) );//true
var_dump( $that_guy->can( 'bar' ) );//true
//unavailable permission
var_dump( $that_guy->can( 'quok' ));//false
//two available permissions
var_dump( $that_guy->can( array( 'foo', 'bar' ) ) );//true
//one available, one unavailable, when requesting for both
//these two lines are exactly identical in functionality
var_dump( $that_guy->can( array( 'foo', 'quok' ) ) );//false
var_dump( $that_guy->can( array( 'foo', 'quok' ), true ) );//false, like previous line
//one available, one unavailable, when requesting for at least one
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 :
class User {
/**
* Logs unauthorized attempts at accessing a permission-protected by the
* current user
* @param perm mixed something representing a permission
*/
public function logUnauthorizedAccess( $perm ) {
//normalization
if ( is_object( $perm ) && ! is_a( $perm, 'Permission' ) ) {
return;
}
elseif ( ( is_string( $perm ) || is_int( $perm ) ) && $perm = new Permission( $perm ) && $perm->getId() ) {
return;
}
/* You can now log the actual wrongful access attempt. */
}
}
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) :
/**
* 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;
public function __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;
}
else return;
}
else return;
}
/**
* 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
*/
public function 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 ) ) {
return true;
}
/* if $this->permissions isn't set, this method has not been called yet
for that script */
if ( empty( $this->permissions ) ) {
$this->fetchPerms();
}
/*
If a single permission is requested, as a string, just check whether
or not it's a key of the User::permissions array
All keys in User::permissions have been granted to the user, and have
1 as an attached value
All ungranted permissions do not appear as keys of User::permissions
*/
if ( is_string( $permission ) ) {
return in_array( $permission, $this->permissions );
}
/*
If not, we'll have to check for all of them, and AND/OR them,
depending on the value of the second parameter.
First, we'll need an array with requested permissions as keys, and 0s
as values, in which we will set 1s for every granted permission
Easy way to do so is to use array_intersect_key, getting the value of
each key of User::$permissions that is present in $permission, then
simply adding an array_fill_keys version of $permission
*/
$checked_permissions = array_intersect_key(
array_fill_keys( $this->permissions, 1 ),
array_fill_keys( $permission, 1 )
) + array_fill_keys( $permission, 0 );
/*
Now we will need array_sum if any of the requested permissions is
sufficient (giving a non-null sum if one of them at least is granted)
or array_product if all of them are needed (giving a null product if
one of them at least is null)
*/
return $and_or ? (bool) array_product( $checked_permissions ) : (bool) array_sum( $checked_permissions );
}
/**
* Fetch permissions for this user from the database, and set the
* User::$permissions property, to allow caching.
* User::$permissions will be an array with permission names as keys.
* @return bool true on success, false on failure
*/
private function fetchPerms() {
$this->permissions = DBSlave::getInstance()
->query( "SELECT users_permissions.permission_id, permissions.permission_name
FROM permissions, users_permissions
WHERE users_permissions.user_id = {$this->id}
AND users_permissions.permission_id = permissions.permission_id" )
->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE );
return is_array( $this->permissions );
}
/**
* Essentially a god mode, gives all currently extant permissions to the
* current user.
* @return bool true on success, false on failure
*/
public function grantAllPerms() {
$allPerms = DBSlave::getInstance()
->query( 'SELECT permission_id FROM permissions' )->fetchAll(PDO::FETCH_COLUMN, 0);
$grantPerm = DBMaster::getInstance()
->prepare( 'INSERT INTO users_permissions (user_id, permission_id)
VALUES (:userid, :permid )
ON DUPLICATE KEY UPDATE permission_id = permission_id');
$grantPerm->bindValue( ':userid', $this->id );
$grantPerm->bindParam( ':permid', $perm );
foreach ( $allPerms as $perm ) {
if ( ! $grantPerm->execute() ) {
return false;
}
}
$this->fetchPerms();
return true;
}
/**
* Grants the current user with a given permission
* @param perm mixed a permission represented by name, id, or object
* @return mixed true on success, false on failure
*/
public function grantPerm( $perm ) {
if ( ! $perm = Permission::normalizeToID( $perm ) ) {
return false;
}
return DBMaster::getInstance()
->prepare( 'INSERT INTO users_permissions (user_id, permission_id)
VALUES (:userid, :permid)' )
->execute( array( ':userid' => $this->id, ':permid' => $perm ) );
}
/**
* Lists permissions granted to the current user
* @refresh bool true to refresh the permission cache for this user
* @return array an id=>name array of all permissions of the current user
*/
public function listPerms( $refresh = false ) {
if ( empty( $this->permissions ) || $refresh ) {
$this->fetchPerms();
}
return $this->permissions;
}
/**
* Sets multiple permissions for the current user.
* Can effectively grant or ungrant permissions, but is more efficient
* when setting multiple permissions than User::grantPerm() and
* User::ungrantPerm()
* @param perms array an array of permissions names, ids, or objects
* @return bool true on success, false on failure
*/
public function setPerms ( $setperms = null ) {
if ( ! is_array( $setperms ) ) {
return false;
}
if ( empty( $setperms ) ) {
return $this->ungrantAllPerms();
}
$this->fetchPerms();
$setperms = array_filter( array_map( array( 'Permission', 'normalizeToID' ), $setperms ) );
$allperms = array_keys( PermissionsList::listAllPerms() );
$adduserperm = array();
$deluserperm = array();
foreach ( $allperms as $perm ) {
if ( ! in_array( $perm, array_keys( $this->permissions ) ) && in_array( $perm, $setperms ) ) {
$adduserperm[] = $perm;
}
}
foreach ( array_keys( $this->permissions ) as $perm ) {
if ( ! in_array( $perm, $setperms ) ) {
$deluserperm[] = $perm;
}
}
if ( ! empty( $adduserperm ) ) {
$permaddstmt = DBMaster::getInstance()
->prepare( 'INSERT INTO users_permissions
VALUES ( :userid, :permid )' );
$permaddstmt->bindValue( ':userid', $this->id );
$permaddstmt->bindParam( ':permid', $addperm );
foreach ( $adduserperm as $addperm ) {
$permaddstmt->execute();
}
}
if ( ! empty( $deluserperm ) ) {
$permdelstmt = DBMaster::getInstance()
->prepare( 'DELETE FROM users_permissions
WHERE user_id = :userid
AND permission_id = :permid' );
$permdelstmt->bindValue( ':userid', $this->id );
$permdelstmt->bindParam( ':permid', $delperm );
foreach ( $deluserperm as $delperm ) {
$permdelstmt->execute();
}
}
$this->fetchPerms();
}
/**
* Essentially a ban, removes all permissions the current user is currently
* granted with
* @return bool true on success, false on failure
*/
public function 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
*/
public function ungrantPerm( $perm ) {
if ( ! $perm = Permission::normalizeToID( $perm ) ) {
return false;
}
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.
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 !
/**
* The permission class itself
* An instance of this class refers to a currently existing permission
*/
class Permission {
protected $id;
protected $name;
protected $userlist;
/**
* Usual constructor. Allows several kinds of param, triggers other
* methods accordingly
* If the Permission is referred to by id, and no permission by that id
* exist, the Permission instance will essentially be empty
* If the Permission is referred to by name, and no permission by that name
* exists, will attempt to create it and set $this->name and $this->id,
* by calling Permission::create()
* @param perm int|string something representing the permission, either
its id or its name
*/
public function __construct( $perm ) {
if ( ( is_int( $perm ) || ctype_digit( $perm ) ) && $perm > 0 ) {
return $this->fetchById( $perm );
}
if ( is_string( $perm ) ) {
return $this->fetchByName( $perm );
}
}
/**
* Fallback function to create permissions on the go. Will be called by the
* constructor (or more accurately Permission::fetchByName()) when provided
* with an unrecognized permission name.
* Once the permission is in the DB, will set $this->id and $this->name
* to conclude __construct functionality
* @param name string the new permission name
*/
private function create( $name ) {
if ( ( DBMaster::getInstance()
->prepare( 'INSERT INTO permissions (permission_id, permission_name)
VALUES (NULL, :newperm)' )
->execute( array( 'newperm' => $name ) ) )
&& ( $id = DBMaster::getInstance()->lastInsertId() ) ) {
$this->name = $name;
$this->id = $id;
return true;
}
else {
unset( $this->name );
return false;
}
}
/**
* Deletes all records regarding the current permission in the database,
* including all links to the users in the users_permissions table.
* @return bool true on success, false on failure (database not writable)
*/
public function delete() {
if ( ! isset( $this->id ) ) {
return true;
}
$a = DBMaster::getInstance()
->prepare( 'DELETE FROM permissions
WHERE permission_id = :deleteperm' )
->execute( array( ':deleteperm' => $this->id ) );
unset( $this->id, $this->name );
return $a;
}
/**
* Fetches a permission using its permission id
* If a permission with this id doesn't exist, will return false
* @param id int
* @return mixed an object representing the permission, or false on failure
*/
private function fetchById( $id ) {
$this->id = $id;
$namestatement = DBSlave::getInstance()
->prepare( 'SELECT permission_name
FROM permissions
WHERE permission_id = :permid' );
$namestatement->execute( array( ':permid' => $id ) );
if ( $namerow = $namestatement->fetch() ) {
$this->name = $namerow['permission_name'];
return true;
}
unset( $this->id, $this->name );
return false;
}
/**
* Fetches a permission using its permission name
* If a permission by this name doesn't exist, will attempt to create one
* @param name string the name of the permission
* @return object an instance of Permission representing the permission by
that name
*/
private function fetchByName( $name ) {
$this->name = $name;
$namestatement = DBSlave::getInstance()
->prepare( 'SELECT permission_id
FROM permissions
WHERE permission_name = :permname' );
$namestatement->execute( array( ':permname' => $name ) );
if ( $namerow = $namestatement->fetch() ) {
$this->id = $namerow['permission_id'];
return true;
}
return $this->create( $name );
}
/**
* Gets the id of the permission
* @return int the id of the permission
*/
public function getId() {
if ( ! isset( $this->id ) ) {
return false;
}
return $this->id;
}
/**
* Gets the name of the permission
* @return string the name of the permission
*/
public function getName() {
if ( ! isset( $this->name ) ) {
return false;
}
return $this->name;
}
/**
* Lists all users who currently are granted that permission
* @param refresh bool whether or not to refresh the cache for the list
of users having that permission
* @return array an array with user ids as keys, usernames as values
*/
public function listUsers( $refresh = false ) {
if ( ! isset( $this->id ) ) {
return array();
}
if ( ! isset( $this->userList ) || $refresh ) {
$userlist = DBMaster::getInstance()
->prepare( 'SELECT users_permissions.user_id, users.user_name
FROM users, users_permissions
WHERE users_permissions.permission_id = :permid
AND users.user_id = users_permissions.user_id' );
$userlist->execute( array( ':permid' => $this->id ) );
$this->userList = $userlist->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_GROUP | PDO::FETCH_UNIQUE );
}
return $this->userList;
}
/**
* Normalizes a permission represented by its name, id, or object
* and returns its id.
* @param mixed perm a permission represented by its name, id, or object
* @return mixed a permission id, or false on failure
*/
public static function normalizeToID( $perm ) {
if ( is_object( $perm ) && is_a( $perm, 'Permission' ) ) {
return $perm->getId();
}
if ( is_string( $perm ) || is_int( $perm ) ) {
$perm = new Permission( $perm );
return $perm->getId();
}
return false;
}
/**
* Normalizes a permission represented by its name, id, or object
* and returns its name
* @param mixed perm a permission represented by its name, id, or object
* @return mixed a permission name, or false on failure
*/
public static function normalizeToName( $perm ) {
if ( is_object( $perm ) && is_a( $perm, 'Permission' ) ) {
return $perm->getName();
}
if ( is_string( $perm ) || is_int( $perm ) ) {
$perm = new Permission( $perm );
return $perm->getName();
}
return false;
}
/**
* Renames a permission in the database
* @param newname string the new name of the permission
* @return bool true on success, false on failure
*/
public function rename( $newname ) {
if ( ! isset( $this->id ) ) {
return true;
}
return DBMaster::getInstance()
->prepare( 'UPDATE permissions
SET permission_name = :newname
WHERE permission_id = :permid' )
->execute( array( ':newname' => $newname, ':permid' => $this->id ) );
}
}
You can obviously add exceptions in the mix.
A way to list all currently existing permissions sure would be nice too. Here goes :
class PermissionsList {
protected static $allPerms;
private function __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 );
}
return self::$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.';
$deleteperm->delete();
}
else {
$message = 'The permission you tried to delete doesn\'t exist';
}
}
/* Display a list all current permissions here */
$permList = PermissionsList::listAllPerms();
?>
<html>
<head>
<title>Choose a permission or create a new one</title>
</head>
<body>
<?php
if ( !empty( $message ) ) {
?>
<div><?php echo $message; ?></div>
<?php
}
?>
<ul>
<?php
foreach ( $permList as $permid => $permname ) {
?>
<li><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>?permid=<?php echo $permid; ?>">(<?php echo $permid; ?>) <?php echo $permname; ?></a></li>
<?php
}
?>
</ul>
<form action="" method="post">
<label for="newperm">Name of the new permission :</label> <input type="text" name="newperm" />
<input type="submit" />
</form>
</form>
</body>
</html>
<?php
return;
}
//create an instance of the Permission class, be it a pre-existing permission
//or a new one
if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) {
$perm = $_POST['newperm'];
}
else {
$perm = $_GET['permid'];
}
//this line will find a permission with the requested id (in the case of an
//existing permission), or attempt to create a new one
$permission = new Permission( $perm );
if ( ! $permission->getId() ) {
echo 'This permission doesn\'t exist';
return;
}
//fetch a refreshed list of all users with the current permission in the DB
$allUsers = $permission->listUsers(true);
?>
<html>
<head>
<title>Users with the <?php echo htmlentities( $permission->getName() ); ?> permission</title>
</head>
<body>
<ul>
<?php
foreach ( $allUsers as $userid => $username ) {
?>
<li><a href="manageuserperms.php?userid=<?php echo $userid; ?>">(<?php echo $userid; ?>) <?php echo htmlentities( $username ); ?></a></li>
<?php
}
?>
</ul>
<form action="<?php echo $_SERVER['SCRIPT_NAME']; ?>" method="post">
<input type="hidden" name="deleteperm" value="<?php echo $permission->getId(); ?>" />
<input type="submit" value="delete this permission" />
</form>
</body>
</html>
<?php
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.
<?php
//create a User instance for the user currently browsing the script.
$currentUser = User::getCurrent();
//check if the current user can grant permissions
if ( ! $currentUser->can( 'giveperms' ) ) {
header( 'Location: error.php?perm=giveperms' );
return;
}
if ( empty( $_GET['userid'] ) ) {
/* Display a list all current users here */
$userList = DBSlave::getInstance()
->query( "SELECT user_id, user_name
FROM users" )
->fetchAll( PDO::FETCH_COLUMN | PDO::FETCH_UNIQUE | PDO::FETCH_GROUP );
?>
<html>
<head>
<title>Choose a user to give permissions to</title>
</head>
<body>
<ul>
<?php
foreach ( $userList as $userid => $username ) {
?>
<li><a href="<?php echo $_SERVER['SCRIPT_NAME']; ?>?userid=<?php echo $userid; ?>">(<?php echo $userid; ?>) <?php echo $username; ?></a></li>
<?php
}
?>
</ul>
</body>
</html>
<?php
return;
}
//create a User instance for the user whose permissions the current user
//is about to edit
$user = new User( $_GET['userid'] );
if ( $_SERVER['REQUEST_METHOD'] == 'POST' && ! empty( $_GET['userid'] ) ) {
//update the database accordingly
if ( empty( $_POST['perms'] ) || ! is_array( $_POST['perms'] ) ) {
$_POST['perms'] = array();
}
$user->setPerms( $_POST['perms'] );
}
//fetch a list of all existing permissions in the database
$allPerms = PermissionsList::listAllPerms();
//refresh the permissions for the target user
$userPerms = $user->listPerms(true);
?>
<html>
<head>
<title>Permissions for <?php echo htmlentities( $user->getName() ); ?></title>
</head>
<body>
<form action="" method="post">
<ul>
<?php
foreach ( $allPerms as $permid => $permname ) {
$checked = in_array( $permname, $userPerms ) ? ' checked="checked"' : '';
?>
<li><input type="checkbox"<?php echo $checked; ?> name="perms[]" id="perm<?php echo $permid; ?>" value="<?php echo $permid; ?>" />
<label for="perm<?php echo $permid; ?>"><?php echo htmlentities( $permname ); ?></label></li>
<?php
}
?>
</ul>
<input type="submit" />
</form>
</body>
</html>
<?php
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.
Creative Commons, Attribution-NonCommercial-ShareAlike license.
Feel free to redistribute or translate it under the terms of the license.
WTF-PL v2 license.
Feel free to comply with that very simple, one-clause, non-viral license 🙂
Leave a Reply