I got everything setup and got all of the test cases to pass except for “testBadPasswordHasNothingAllowed”. (SimpleTest is a nice tool, BTW. I had never used it before this.)
Here is the working code for our first iteration:
In file “rbac.sql”:
create table `usage` (
id int(11) not null auto_increment primary key,
name varchar(255)
);
create table role (
id int(11) not null auto_increment primary key,
name varchar(255)
);
create table perm (
id int(11) not null auto_increment primary key,
name varchar(255)
);
create table usage_assign (
usage_id int(11) not null default 0,
role_id int(11) not null default 0,
primary key(usage_id, role_id)
);
create table perm_assign (
role_id int(11) not null default 0,
perm_id int(11) not null default 0,
primary key(role_id, perm_id)
);
In file “authoriser.php”:
<?php
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'permissions.php');
require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'cache.php');
class Authoriser {
var $_storage;
function Authoriser(&$storage) {
$this->_storage = &$storage;
}
function &getPermissions($name) {
$finder = &new PermissionsCache($this->_storage->createPermissionsFinder());
return new Permissions($finder->findAllByName($name));
}
function &createEdit() {
return $this->_storage->createEdit();
}
}
?>
In file “cache.php”:
<?php
class PermissionsCache {
var $_finder;
function PermissionsCache(&$finder) {
$this->_finder = &$finder;
}
function &findAllByName($name) {
return $this->_finder->findAllByName($name);
}
}
?>
In file “pear_storage.php”:
<?php
require_once("DB.php");
require_once("permissions.php");
define('USAGE_TABLE', '`usage`');
define('USAGE_TABLE_ID', 'id');
define('USAGE_TABLE_NAME', 'name');
define('ROLE_TABLE', 'role');
define('ROLE_TABLE_ID', 'id');
define('ROLE_TABLE_NAME', 'name');
define('PERM_TABLE', 'perm');
define('PERM_TABLE_ID', 'id');
define('PERM_TABLE_NAME', 'name');
define('USAGE_ASSIGN_TABLE', 'usage_assign');
define('USAGE_ASSIGN_TABLE_USAGE_ID', 'usage_id');
define('USAGE_ASSIGN_TABLE_ROLE_ID', 'role_id');
define('PERM_ASSIGN_TABLE', 'perm_assign');
define('PERM_ASSIGN_TABLE_ROLE_ID', 'role_id');
define('PERM_ASSIGN_TABLE_PERM_ID', 'perm_id');
class PearAuthorisationStorage {
var $_connection;
function PearAuthorisationStorage($connection_string) {
$this->_connection = &$this->_createConnection($connection_string);
}
function createPermissionsFinder() {
return new PearPermissionsFinder($this->_connection);
}
function &createEdit() {
return new PearAuthorisationEdit($this->_connection);
}
function &_createConnection($connection_string) {
$db =& DB::connect($connection_string);
if (DB::isError($db)) {
die($db->getMessage());
}
return $db;
}
}
class PearAuthorisationEdit {
var $_connection;
function PearAuthorisationEdit(&$connection) {
$this->_connection =& $connection;
}
function addUsage($usage) {
$gateway =& new PearUsageGateway($this->_connection);
$gateway->setName($usage);
$gateway->insert();
}
function dropUsage($usage) {
$gateway =& new PearUsageGateway($this->_connection);
$gateway->setName($usage);
$gateway->delete();
}
function addRole($role) {
$gateway =& new PearRoleGateway($this->_connection);
$gateway->setName($role);
$gateway->insert();
}
function dropRole($role) {
$gateway =& new PearRoleGateway($this->_connection);
$gateway->setName($role);
$gateway->delete();
}
function addPermission($action) {
$gateway =& new PearPermissionGateway($this->_connection);
$gateway->setName($action);
$gateway->insert();
}
function dropPermission($action) {
$gateway =& new PearPermissionGateway($this->_connection);
$gateway->setName($action);
$gateway->delete();
}
// Usage Assignment: Assign Roles to Usages
function assign($usage, $role) {
// Row Data Gateway access for Usage table
$finder =& new PearUsageFinder($this->_connection);
$gateway =& $finder->findByName($usage);
$usageId = $gateway->getUsageId();
// Row Data Gateway access for Role Table
$finder =& new PearRoleFinder($this->_connection);
$gateway =& $finder->findByName($role);
$roleId = $gateway->getRoleId();
// Row Data Gateway access for Usage Assignment Table
$gateway =& new PearUsageAssignmentGateway($this->_connection);
$gateway->setUsageId($usageId);
$gateway->setRoleId($roleId);
$gateway->insert();
}
// Permission Assignment (PA): Assign Permissions to Roles
function permit($role, $action) {
// Row Data Gateway access for Role Table
$finder =& new PearRoleFinder($this->_connection);
$gateway =& $finder->findByName($role);
$roleId = $gateway->getRoleId();
// Row Data Gateway access for Permission Table
$finder =& new PearPermissionFinder($this->_connection);
$gateway =& $finder->findByName($action);
$permissionId = $gateway->getPermissionId();
// Row Data Gateway access for Permission Assignment Table
$gateway =& new PearPermissionAssignmentGateway($this->_connection);
$gateway->setRoleId($roleId);
$gateway->setPermissionId($permissionId);
$gateway->insert();
}
function commit() {
// ...
}
}
// Data Mapper/Finder for Permissions object (Domain Object)
class PearPermissionsFinder {
var $_connection;
function PearPermissionsFinder(&$connection) {
$this->_connection =& $connection;
}
function &findAllByName($name) {
$sql = "SELECT p." . PERM_TABLE_NAME . " AS permission
FROM
" . USAGE_ASSIGN_TABLE . " ua,
" . PERM_ASSIGN_TABLE . " pa,
" . PERM_TABLE . " p,
" . USAGE_TABLE . " u
WHERE u." . USAGE_TABLE_NAME . " = '$name' AND
ua." . USAGE_ASSIGN_TABLE_USAGE_ID . " = u." . USAGE_TABLE_ID . " AND
ua." . USAGE_ASSIGN_TABLE_ROLE_ID . " = pa." . PERM_ASSIGN_TABLE_ROLE_ID . " AND
pa." . PERM_ASSIGN_TABLE_PERM_ID . " = p." . PERM_TABLE_ID;
$result =& $this->_connection->query($sql);
if (DB::isError($result)) {
die($result->getMessage());
}
$all = array();
while ($row =& $result->fetchRow(DB_FETCHMODE_ASSOC)) {
$all[] = $row['permission'];
}
$result->free();
// return new Permissions($all);
return $all;
}
}
class PearUsageAssignmentGateway {
var $_connection;
var $_roleId;
var $_usageId;
function PearUsageAssignmentGateway(&$connection, $roleId = 0, $usageId = 0) {
$this->_connection =& $connection;
$this->_roleId = $roleId;
$this->_usageId = $usageId;
}
function setRoleId($roleId) {
$this->_roleId = $roleId;
}
function setUsageId($usageId) {
$this->_usageId = $usageId;
}
function insert() {
$sql = "INSERT INTO " . USAGE_ASSIGN_TABLE . " VALUES (" . $this->_usageId . ", " . $this->_roleId . ")";
$this->_connection->query($sql);
if (DB::isError($this->_connection)) {
die($this->_connection->getMessage());
}
}
function delete() {
$sql = "DELETE FROM " . USAGE_ASSIGN_TABLE . " WHERE " . USAGE_ASSIGN_TABLE_ROLE_ID . "=" . $this->_roleId . " AND " . USAGE_ASSIGN_TABLE_USAGE_ID . "=" . $this->_usageId;
$this->_connection->query($sql);
if (DB::isError($this->_connection)) {
die($this->_connection->getMessage());
}
}
}
class PearPermissionAssignmentGateway {
var $_connection;
var $_permissionId;
var $_roleId;
function PearPermissionAssignmentGateway(&$connection, $permissionId = 0, $roleId = 0) {
$this->_connection =& $connection;
$this->_permissionId = $permissionId;
$this->_roleId = $roleId;
}
function setPermissionId($permissionId) {
$this->_permissionId = $permissionId;
}
function setRoleId($roleId) {
$this->_roleId = $roleId;
}
function insert() {
$sql = "INSERT INTO " . PERM_ASSIGN_TABLE . " VALUES (" . $this->_roleId . ", " . $this->_permissionId . ")";
$this->_connection->query($sql);
if (DB::isError($this->_connection)) {
die($this->_connection->getMessage());
}
}
function delete() {
$sql = "DELETE FROM " . PERM_ASSIGN_TABLE . " WHERE " . PERM_ASSIGN_TABLE_PERM_ID . "=" . $this->_permissionId . " AND " . PERM_ASSIGN_TABLE_ROLE_ID . "=" . $this->_roleId;
$this->_connection->query($sql);
if (DB::isError($this->_connection)) {
die($this->_connection->getMessage());
}
}
}
class PearUsageGateway {
var $_connection;
var $_id;
var $_name;
function PearUsageGateway(&$connection) {
$this->_connection =& $connection;
}
function setName($name) {
$this->_name = $name;
}
function getName() {
return $this->_name;
}
function setUsageId($id) {
$this->_id = $id;
}
function getUsageId() {
return $this->_id;
}
function insert() {
$sql = "INSERT INTO " . USAGE_TABLE . " (" . USAGE_TABLE_NAME . ") VALUES ('" . $this->_name . "')";
$this->_connection->query($sql);
if (DB::isError($this->_connection)) {
die($this->_connection->getMessage());
}
}
function update() {
$this->_connection->query("UPDATE " . USAGE_TABLE . " SET " . USAGE_TABLE_NAME . "='" . $this->_name . "' WHERE " . USAGE_TABLE_ID . "=" . $this->_id);
}
function delete() {
// delete by name
$this->_connection->query("DELETE FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_NAME . "='" . $this->_name . "'");
}
function &load(&$row) {
$gateway =& new PearUsageGateway($this->_connection);
$gateway->setUsageId($row['id']);
$gateway->setName($row['name']);
return $gateway;
}
}
class PearUsageFinder {
var $_connection;
function PearUsageFinder(&$connection) {
$this->_connection = $connection;
}
function &findByName($name) {
$sql = "SELECT * FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_NAME . "='$name'";
$result =& $this->_connection->query($sql);
$row =& $result->fetchRow(DB_FETCHMODE_ASSOC);
return PearUsageGateway::load($row);
}
}
class PearRoleGateway {
var $_connection;
var $_id;
var $_name;
function PearRoleGateway(&$connection) {
$this->_connection =& $connection;
}
function setName($name) {
$this->_name = $name;
}
function getName() {
return $this->_name;
}
function setRoleId($id) {
$this->_id = $id;
}
function getRoleId() {
return $this->_id;
}
function insert() {
$this->_connection->query("INSERT INTO " . ROLE_TABLE . " (" . ROLE_TABLE_NAME . ") VALUES ('" . $this->_name . "')");
}
function update() {
$this->_connection->query("UPDATE " . ROLE_TABLE . " SET " . ROLE_TABLE_NAME . "='" . $this->_name . "' WHERE " . ROLE_TABLE_ID . "=" . $this->_id);
}
function delete() {
// delete by name
$this->_connection->query("DELETE FROM " . ROLE_TABLE . " WHERE " . ROLE_TABLE_NAME . "='" . $this->_name . "'");
}
function &load(&$row) {
$gateway =& new PearRoleGateway($this->_connection);
$gateway->setRoleId($row['id']);
$gateway->setName($row['name']);
return $gateway;
}
}
class PearRoleFinder {
var $_connection;
function PearRoleFinder(&$connection) {
$this->_connection =& $connection;
}
function &findByName($name) {
$sql = "SELECT * FROM " . ROLE_TABLE . " WHERE " . ROLE_TABLE_NAME . "='$name'";
$result =& $this->_connection->query($sql);
$row =& $result->fetchRow(DB_FETCHMODE_ASSOC);
return PearRoleGateway::load($row);
}
}
class PearPermissionGateway {
var $_connection;
var $_id;
var $_name;
function PearPermissionGateway(&$connection) {
$this->_connection =& $connection;
}
function setName($name) {
$this->_name = $name;
}
function getName() {
return $this->_name;
}
function setPermissionId($id) {
$this->_id = $id;
}
function getPermissionId() {
return $this->_id;
}
function insert() {
$this->_connection->query("INSERT INTO " . PERM_TABLE . " (" . PERM_TABLE_NAME . ") VALUES ('" . $this->_name . "')");
}
function update() {
$this->_connection->query("UPDATE " . PERM_TABLE . " SET " . PERM_TABLE_NAME . "='" . $this->_name . "' WHERE " . PERM_TABLE_ID . "=" . $this->_id);
}
function delete() {
// delete by name
$this->_connection->query("DELETE FROM " . PERM_TABLE . " WHERE " . PERM_TABLE_NAME . "='" . $this->_name . "'");
}
function &load(&$row) {
$gateway =& new PearPermissionGateway($this->_connection);
$gateway->setPermissionId($row['id']);
$gateway->setName($row['name']);
return $gateway;
}
}
class PearPermissionFinder {
var $_connection;
function PearPermissionFinder(&$connection) {
$this->_connection =& $connection;
}
function &findByName($name) {
$sql = "SELECT * FROM " . PERM_TABLE . " WHERE " . PERM_TABLE_NAME . "='$name'";
$result =& $this->_connection->query($sql);
$row =& $result->fetchRow(DB_FETCHMODE_ASSOC);
return PearPermissionGateway::load($row);
}
}
?>
In file “permissions.php”:
<?php
class Permissions {
var $_all;
function Permissions(&$all) {
$this->_all =& $all;
}
function can($action) {
return in_array($action, $this->_all);
}
}
?>
In file “test.php”:
<?php
if (!defined(SIMPLE_TEST)) {
define('SIMPLE_TEST', '../simpletest/');
}
define('CONNECTION_STRING', 'mysql://user:pass@localhost/rbac');
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');
require_once('authoriser.php');
require_once('pear_storage.php');
class RoleBasedPermissionsTest extends UnitTestCase {
function RoleBasedPermissionsTest() {
$this->UnitTestCase();
$this->storage = &new PearAuthorisationStorage(CONNECTION_STRING);
}
function setUp() {
$authoriser = &new Authoriser($this->storage);
$edit = &$authoriser->createEdit();
$edit->addUsage('fred');
$edit->addRole('pleb');
$edit->addPermission('do_stuff');
$edit->assign('fred', 'pleb');
$edit->permit('pleb', 'do_stuff');
$edit->commit();
}
function tearDown() {
$authoriser = &new Authoriser($this->storage);
$edit = &$authoriser->createEdit();
$edit->dropUsage('fred');
$edit->dropRole('pleb');
$edit->dropPermission('do_stuff');
$edit->commit();
}
function testNonUserHasNothingAllowed() {
$authoriser = &new Authoriser($this->storage);
$permissions = &$authoriser->getPermissions('public');
$this->assertFalse($permissions->can('do_stuff'));
}
/*
function testBadPasswordHasNothingAllowed() {
$authoriser = &new Authoriser($this->storage);
$permissions = &$authoriser->getPermissions('fred');
$this->assertFalse($permissions->can('do_stuff'));
}
*/
function testLegitimateUserHasActionAllowed() {
$authoriser = &new Authoriser($this->storage);
$permissions = &$authoriser->getPermissions('fred');
$this->assertTrue($permissions->can('do_stuff'));
}
function testUserCannotDoNonAction() {
$authoriser = &new Authoriser($this->storage);
$permissions = &$authoriser->getPermissions('fred');
$this->assertFalse($permissions->can('do_unknown'));
}
}
$test =& new RoleBasedPermissionsTest();
$test->run(new HtmlReporter());
?>
Obviously we have a long way to go with this. I can already see several refactorings. Also, we need to cascade deletes if a user, role, or pemrission is dropped. Which means the row data gateway isn’t sufficient for the two connective tables (usage_asisgn and perm_assign). We will need to use a Table Data Gateway to manage multiple rows on cascading deletes. Anyways, try setting it up and see if you can get similar results.
Thanks,
JT