PHP Access Control - PHP5 CMS Framework Development

Framework Solution

The implementation of access control falls into three classes. One is the class that is asked questions about who can do what. Closely associated with this is another class that caches general information applicable to all users. It is made a separate class to aid implementation of the split of cache between generalD and user specifi c. The third class handles administration operations. Before looking at the classes, though, let's fi gure out the database design.

Database for RBAC

All that is required to implement basic RBAC is two tables. A third table is required to extend to a hierarchical model. An optional extra table can be implemented to hold role descriptions. Thinking back to the design considerations, the fi rst need is for a way to record the operations that can be done on the subjects, that is the permissions. They are the targets for our access control system. You'll recall that a permission consists of an action and a subject, where a subject is defi ned by a type, and an identifi er. For ease of handling, a simple auto-increment ID number is added. But we also need a couple of other things.

To make our RBAC system general, it is important to be able to control not only the actual permissions, but also who can grant those permissions, and whether they can grant that right to others. So an extra control fi eld is added with one bit for each of those three possibilities. It therefore becomes possible to grant the right to access something with or without the ability to pass on that right.

The other extra data item that is useful is a "system" fl ag. It is used to make some permissions incapable of deletion. Although not being a logical requirement, this is certainly a practical requirement. We want to give administrators a lot of power over the confi guration of access rights, but at the same time, we want to avoid any catastrophes. The sort of thing that would be highly undesirable would be for the top level administrator to remove all of their own rights to the system. In practice, most systems will have a critical central structure of rights, which should not be altered even by the highest administrator.

So now the permissions table can be seen to be as shown in the following screenshot:

Note that the character strings for role, action, and subject_type are given generous lengths of 60, which should be more than adequate. The subject ID will often be quite short, but to avoid constraining generality, it is made a text fi eld, so that the RBAC system can still handle very complex identifi ers, if required. Of course, there will be some performance penalties if this fi eld is very long, but it is better to have a design trade-off than a limitation. If we restricted the subject ID to being a number, then more complex identifi ers would be a special case. This would destroy the generality of our scheme, and might ultimately reduce overall effi ciency. In addition to the auto-increment primary key ID, two indices are created, as shown in the following screenshot. They involve overhead during update operations but are likely to speed access operations. Since far more accesses will typically be made than updates, this makes sense. If for some reason an index does not give a benefi t, it is always possible to drop it. Note that the index on the subject ID has to be constrained in length to avoid breaking limits on key size. T he value chosen is a compromise between effi ciency through short keys, and effi ciency through the use of fi ne grained keys. In a heavily used system, it would be worth reviewing the chosen fi gure carefully, and perhaps modifying it in the light of studies into actual data.

The other main database table is even simpler, and holds information about assignment of accessors to roles. Again, an auto-increment ID is added for convenience. Apart from the ID, the only fi elds required are the role, the accessor type, and the accessor ID. This time a single index, additional to the primary key, is suffi cient. The assignment table is shown in the following screenshot, and its index is shown in the screenshot after that:

Adding hierarchy to RBAC requires only a very simple table, where each row contains two fi elds: a role, and an implied role. Both fi elds constitute the primary key, neither fi eld on its own being necessarily unique. An index is not required for effi ciency, since the volume of hierarchy information is assumed to be small, and whenever it is needed, the whole table is read. But it is still a good principle to have a primary key, and it also guarantees that there will not be redundant entries. For the example given earlier, a typical entry might have consultant as the role, and doctor as the implied role. At present, Aliro implements hierarchy only for backwards compatibility, but it is a relatively easy development to make hierarchical relationships generally available.

Opti onally, an extra table can be used to hold a description of the roles in use. This has no functional purpose, and is simply an option to aid administrators of the system. The table should have the role as its primary key. As it does not affect the functionality of the RBAC at all, no further detail is given here.

With the database design settled, let's look at the classes. The simplest is the administration class, so we'll start there.

Administering RBAC

The administration of the system could be done by writing directly to the database, since that is what most of the operations involve. There are strong reasons not to do so. Although the operations are simple, it is vital that they be handled correctly. It is generally a poor principle to allow access to the mechanisms of a system rather than providing an interface through class methods. The latter approach ideally allows the creation of a robust interface that changes relatively infrequently, while details of implementation can be modifi ed without affecting the rest of the system.

The administration class is kept separate from the classes handling questions about access because for most CMS requests, administration will not be needed, and the administration class will not load at all. As a central service, the class is implemented as a standard singleton, but it is not cached because information generally needs to be written immediately to the database. In fact, the administration class frequently requests the authorization cache class to clear its cache so that the changes in the database can be effective immediately. The class starts off:

class aliroAuthorisationAdmin
{
private static $instance = __CLASS__;
private $handler = null;
private $authoriser = null;
private $database = null;
private function __construct()
{
$this->handler =& aliroAuthoriserCache::getInstance();
$this->authoriser =& aliroAuthoriser::getInstance();
$this->database = aliroCoreDatabase::getInstance();
}
private function __clone()
{
// Enforce singleton
}
public static function getInstance()
{
return is_object(self::$instance) ? self::$instance :
(self::$instance = new self::$instance());
}
private function doSQL($sql, $clear=false)
{
$this->database->doSQL($sql);
if ($clear) $this->clearCache();
}
private function clearCache()
{
$this->handler->clearCache();
}

Apart from the instance property that is used to implement the singleton pattern, the other private properties are related objects that are acquired in the constructor to help other methods. Getting an instance operates in the usual fashion for a singleton, with the private constructor, and clone methods enforcing access solely via getInstance.

The doSQL method also simplifi es other methods by combining a call to the database with an optional clearing of cache through the class's clearCache method. Clearly the latter is simple enough that it could be eliminated. But it is better to have the method in place so that if changes were made to the implementation such that different actions were needed when any relevant cache is to be cleared, the changes would be isolated to the clearCache method. Next we have a couple of useful methods that simply refer to one of the other RBAC classes:

public function getAllRoles($addSpecial=false)
{
return $this->authoriser->getAllRoles($addSpecial);
}
public function getTranslatedRole($role)
{
return $this->authoriser->getTranslatedRole($role);
}

Again, these are provided so as to simplify the future evolution of the code so that implementation details are concentrated in easily identifi ed locations. The general idea of getAllRoles is obvious from the name, and the parameter determines whether the special roles such as visitor, registered, and nobody will be included. Since those roles are built into the system in English, it would be useful to be able to get local translations for them. So the method getTranslatedRole will return a translation for any of the special roles; for other roles it will return the parameter unchanged, since roles are created dynamically as text strings, and will therefore normally be in a local language from the outset. Now we are ready to look at the fi rst meaty method:

public function permittedRoles ($action, $subject_type, $subject_id)
{
$nonspecific = true;
foreach ($this->permissionHolders ($subject_type, $subject_id)
as $possible)
{
if ('*' == $possible->action OR $action == $possible->action)
{
$result[$possible->role] = $this->getTranslatedRole
($possible->role);
if ('*' != $possible->subject_type AND '*' !=
$possible_subject_id) $nonspecific = false;
}
}
if (!isset($result))
{
if ($nonspecific) $result = array('Visitor' =>
$this->getTranslatedRole('Visitor'));
else return array();
}
return $result;
}
private function &permissionHolders ($subject_type, $subject_id)
{
$sql = "SELECT DISTINCT role, action, control, subject_type,
subject_id FROM #__permissions";
if ($subject_type != '*') $where[] =
"(subject_type='$subject_type' OR subject_type='*')";
if ($subject_id != '*') $where[] = "(subject_id='$subject_id' OR
subject_id='*')";
if (isset($where)) $sql .= " WHERE ".implode(' AND ', $where);
return $this->database->doSQLget($sql);
}

Any code that is providing an RBAC administration function for some part of the CMS is likely to want to know what roles already have a particular permission so as to show this to the administrator in preparation for any changes. The private method permissionHolders uses the parameters to create a SQL statement that will obtain the minimum relevant permission entries. This is complicated by the fact that in most contexts, asterisk can be used as a wild card.

The public method permittedRoles uses the private method to obtain relevant database rows from the permissions table. These are checked against the action parameter to see which of them are relevant. If there are no results, or if none of the results refer specifi cally to the subject, without the use of wild cards, then it is assumed that all visitors can access the subject, so the special role of visitor is added to the results. When actual permission is to be granted we need the following methods:

public function permit ($role, $control, $action, $subject_type, $subject_id)
{
$sql = $this->permitSQL($role, $control, $action, $subject_type,
$subject_id);
$this->doSQL($sql, true);
}
private function permitSQL ($role, $control, $action, $subject_type, $subject_id)
{
$this->database->setQuery("SELECT id FROM #__permissions WHERE
role='$role' AND action='$action' AND
subject_type='$subject_type' AND
subject_id='$subject_id'");
$id = $this->database->loadResult();
if ($id) return "UPDATE #__permissions SET control=$control WHERE id=$id";
else return "INSERT INTO #__permissions (role, control, action,
subject_type, subject_id) VALUES ('$role', '$control',
'$action', '$subject_type', '$subject_id')";
}

The public method permit grants permission to a role. The control bits are set in the parameter $control. The action is part of permission, and the subject of the action is identifi ed by the subject type and identity parameters. Most of the work is done by the private method that generates the SQL; it is kept separate so that it can be used by other methods. Once the SQL is obtained, it can be passed to the database, and since it will normally result in changes, the option to clear the cache is set.

Article Type: 
How-to
0
Average: 5 (1 vote)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)