Patterns Tutorial Series (part 1): RBAC Domain Model

Hi…

Still thinking aloud, what if the usage were like this…?


$authoriser = &new Authoriser(new MySqlStorage(), New RbacMechanism());

That is the choice of administration could be configured within the app. as well. I don’t tink this should be added yet (I don’t see much commonality and its scope creep again).

Also, after a discussion with Meryn in another thread, I think too much construction is happening if a configuration file is passed. Clients of the library can subclass for this behaviour.

Another go just to establish a reference skeleton…


<?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'));
    }
}
?>

I’ll try to get these tests to run again (and probably embarrass myself again) in the next post.

yours, Marcus

Hi…

Here is enough to get some structure into teh design. Hack to taste.

Here is a top level 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();
    }
}
?>

The cache in cache.php does nothing…


<?php
class PermissionsCache {
    var $_finder;

    function PermissionsCache(&$finder) {
        $this->_finder = &$finder;
    }
    function findAllByName($name) {
        return $this->_finder->findAllByName($name);
    }
}
?>

The permissions class in permissions.php has a long way to go…


<?php
class Permissions {
    function Permissions($all) {
    }
    function can() {
    }
}
?>

I guess it just takes a list in the constructor.

All of the real work is in the sample storage classes, here pear_storage.php as an example…


<?php
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) {
    }
}

class PearPermissionsFinder {
    function PearPermissionsFinder(&$connection) {
    }
    function findAllByName($name) {
    }
}

class PearAuthorisationEdit {

    function PearAuthorisationEdit(&$connection) {
    }
    function addUsage($name) {
    }
    function dropUsage($name) {
    }
    function addRole($role) {
    }
    function dropRole($role) {
    }
    function addPermission($action) {
    }
    function dropPermission($action) {
    }
    function assign($name, $role) {
    }
    function permit($role, $action) {
    }
    function commit() {
    }
}
?>

It should be possible to flesh these out. The only tricky one is the cache as this needs to be configurable just like the deep storage. This means changing the interface again :(.

Any takers? I wouldn’t want to end up writing the whole thing, but I will if no one manages to stop me :).

yours, Marcus

Thanks, Marcus. I will look over your code and see what “blanks” I can “fill-in”.

JT

It seems that since we are not really using a DomainModel here that a the TableGateway pattern will work better than a DataMapper (especially in the PearAuthorisationEdit class). I am under the impression that a DataMapper is useful only when you are returning a Domain Object (or a collection of the Domain Objects) to the client. The PermissionsFinder can still work (as the Permissions object is a Domain Object), I’m mainly talking about the AuthorisationEdit class.

JT

Here is my “stab” at Permissions, PearAuthorisationStorage, and PearPermissionFinder:


<?php
class Permissions {
    var $_all;

    function Permissions(&$all) {
        $this->_all &= $all;
    }

    function can($action) {
        return in_array($action, $this->_all);
    }
}
?>


<?php
require_once("DB.php");

// Same format as HarryF's PHP Anthology
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) {
        return DB::connect($connection_string);
    }
}

// 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);

        $all = array();
        while ($row =& $result->fetchRow(DB_FETCHMODE_ASSOC)) {
            $all[] = $row['permission'];
        }

        $result->free();

        return new Permissions($all);
    }
}

Comments are welcome (appreciated) :wink:

JT

PearRoleAssignmentGateway implementation:


<?php
define('ROLE_ASSIGN_TABLE', 'role_assign');
define('ROLE_ASSIGN_TABLE_ROLE_ID', 'role_id');
define('ROLE_ASSIGN_TABLE_USAGE_ID', 'usage_id');

// Will be used by PearAuthorisationEdit::assign($name, $role)
class PearRoleAssignmentGateway {
    var $_connection;
    var $_roleId;
    var $_usageId;

    function PearRoleAssignmentGateway(&$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() {
        $this->_connection->query("INSERT INTO " . ROLE_ASSIGN_TABLE . "VALUES (" . $this->_usageId . ", " . $this->_roleId . ")");
    }

    function delete() {
        $this->_connection->query("DELETE FROM " . ROLE_ASSIGN_TABLE . " WHERE " . ROLE_ASSIGN_TABLE_ROLE_ID . "=" . $this->_roleId . " AND " . ROLE_ASSIGN_TABLE_USAGE_ID . "=" . $this->_usageId);
    }
}
?>

PearPermissionAssignmentGateway implementation:


<?php
define('PERM_ASSIGN_TABLE', 'perm_assign');
define('PERM_ASSIGN_TABLE_PERM_ID', 'perm_id');
define('PERM_ASSIGN_TABLE_ROLE_ID', 'role_id');

// Will be used by PearAuthorisationEdit::permit($role, $action)
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() {
        $this->_connection->query("INSERT INTO " . PERM_ASSIGN_TABLE . " VALUES (" . $this->_permissionId . ", " . $this->_roleId . ")");
    }

    function delete() {
        $this->_connection->query("DELETE FROM " . PERM_ASSIGN_TABLE . " WHERE " . PERM_ASSIGN_TABLE_PERM_ID . "=" . $this->_permissionId . " AND " . PERM_ASSIGN_TABLE_ROLE_ID . "=" . $this->_roleId);
    }
}

PearAuthorisationEdit implementation:


class PearAuthorisationEdit {
    var $_connection;

    function PearAuthorisationEdit(&$connection) {
        $this->_connection =& $connection;
    }

    function addUsage($usage) {
        $gateway =& PearUsageGateway($this->_connection);
        $gateway->setName($usage);
        $gateway->insert();
    }

    function dropUsage($usage) {
        $gateway =& PearUsageGateway($this->_connection);
        $gateway->setName($usage);
        $gateway->delete();
    }

    function addRole($role) {
        $gateway =& PearRoleGateway($this->_connection);
        $gateway->setName($role);
        $gateway->insert();
    }

    function dropRole($role) {
        $gateway =& PearRoleGateway($this->_connection);
        $gateway->setName($role);
        $gateway->delete();
    }

    function addPermission($action) {
        $gateway =& PearPermissionGateway($this->_connection);
        $gateway->setName($action);
        $gateway->insert();
    }

    function dropPermission($action) {
        $gateway =& PearPermissionGateway($this->_connection);
        $gateway->setName($action);
        $gateway->delete();
    }

    // Role Assignment (RA): Assign Roles to Usages
    function assign($usage, $role) {
        // Row Data Gateway access for Usage table
        $finder =& PearUsageFinder($this->_connection);
        $finder->findByName($usage);
        $usageId = $finder->getUsageId();

        // Row Data Gateway access for Role Table
        $finder =& PearRoleFinder($this->_connection);
        $finder->findByName($role);
        $roleId = $finder->getRoleId();

        // Row Data Gateway access for RoleAssignment Table
        $gateway =& new PearRoleAssignmentGateway($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 &= PearRoleFinder($this->_connection);
        $finder->findByName($role);
        $roleId = $finder->getRoleId();

        // Row Data Gateway access for Permission Table
        $finder &= PearPermissionFinder($this->_connection);
        $finder->findByName($action);
        $permissionId = $finder->getPermissionId();

        // Row Data Gateway access for PermissionAssignment Table
        $gateway &= PearPermissionAssignmentGateway($this->_connection);
        $gateway->setRoleId($roleId);
        $gateway->setPermissionId($permissionId);
        $gateway->insert();
    }
}

That leaves the following classes to be implemented: PearUsageGateway (setName/insert/delete), PearRoleGateway (setName/insert/delete), PearPermissionGateway (setName/insert/delete), PearUsageFinder (findByName/getUsageId), PearRoleFinder (findByName/getRoleId), and PearPermissionFinder (findByName/getPermissionId).

I think they should all be pretty trivial to implement as above.

Thanks,

JT

I’m thinking that the Row Data Gateways left to be implemented will take on the following form (attempting to strictly adhere to Fowler’s description):


<?php
define('USAGE_TABLE', 'usage');
define('USAGE_TABLE_NAME', 'name');
define('USAGE_TABLE_ID', 'id');

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() {
        $this->_connection->query("INSERT INTO " . USAGE_TABLE . " (" . USAGE_TABLE_NAME . ") VALUES ('" . $this->_name . "')");
    }

    function update() {
        $this->_connection->query("UPDATE " . USAGE_TABLE . " SET " . USAGE_TABLE_NAME . "='" . $this->_name . "' WHERE " . USAGE_TABLE_ID . "=" . $this->_id);
    }

    function delete() {
//      $this->_connection->query("DELETE FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_ID . "=" . $this->_id);

        // delete by name
        $this->_connection->query("DELETE FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_NAME . "='" . $this->_name . "'");
    }

    function &load(&$result) {
        $gateway =& new PearUsageGateway($this->_connection);
        $gateway->setUsageId($result['id']);
        $gateway->setName($result['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);
        return PearUsageGateway::load($result);
    }
}
?>

JT

Hi…

We seemed to have dropped below that level. We do have domain objects above, the Permissions object. Authoriser is technical, but an application can subclass it into the domain…


class LoginAuthoriser extends Authoriser {
    function LoginAuthoriser() {
        $this->Authoriser(new PearAuthorisationStorage());
    }
    function &createEdit() {
        return new LoginEdit(parent::createEdit());
    }
}

Normally the config. file would be brought in here, but you get the idea.

The LoginEdit can expose just domain significant operations, such as addPublicUser() or addAuthor() and build them from addUsage(), etc. and commit().

That makes the third domain class and I quite like it this way :).

Agree 100%. Because the model is held on the database and is quite complicated and because we are exposing a data structure, we have the situation where we are doing incremental edits. DataMapper just doesn’t seem to work with anything less a complete domain concept. Hm…interesting.

yours, Marcus

I fixed some SQL and removed “update” function from UserAssignmentGateway and PermissionAssignmentGateway.

JT

Hi everyone.

[left]I’ve had some time to read what is being proposed in regard to design and implementation.
[/left]

[left]It seems to me that everyone is moving somewhat too quickly. The reason I say this is that no one has really discussed what the design requirements of the data structure are and how the application will benefit from one layout versus another.
[/left]

[left]1) Before we jump into a implementation that may not fulfill the requirements we seek I propose we discuss the design of the Data Structures (session and disk). Does anyone agree?
[/left]

[left]2) One of the issues I ran into in designing a Authorization model is that of the framework and it’s structure. I think this was overlooked. What are the requirements of the framework - if there is one?
[/left]

[left]3) What are the general design goals that we wish to accomplish? It may be prudent to list them, though they may be well known, so that we can simply mark them off as accomplished.
[/left]

[left]I want to keep it simple - for me. I know this is a vast project and it could easily become confusing as to where exactly we are headed, utility wise.
[/left]

[left]Great work so far! :wink:
[/left]

[left]Can’t wait to hear what you all think, I’m all ready to dive in.
[/left]

Hi…

I think it is just a case of start the ball rolling and see who runs with it :). Actually I think we have managed to hide the data structures to a large degree as the PearAuthorisationStorage is one of many possibilities. What did you have in mind?

Minimal enough to fit in a tutorial, which means smallest possible scope. Storage/schema nuetral and remotable are what we have ended up with. It was kind of described in a few posts, a starting test case punted as the spec. and top down from there.

It’s open house - dive in :).

yours, Marcus

Hi…

You have certainly churned through this at a terrific rate :). As much as to examine all sides as anything else, here are contradictory thoughts in no order. LIke a whiteboard discussion with no whiteboard…

  1. Do we have two layers of abstraction here? That is the TableGateways and the Storage (basically DataAccessor). Could everything just be coded straight into the storage classes?

  2. Need transactions of course.

  3. Some stuff looks repetitive now. Can we extract some commonality into a gateway base class?

  4. For file based approaches that are written and read in one go, it makes sense to use a data mapper. We haven’t implemented this yet, but I can see how it would work out. Load (constructor) becomes parsing and commit() becomes marshalling. One model and lot’s of mappers would cover different file formats, all hidden behind teh storage classes.

  5. In what other ways can the code now be made smaller and simpler?

  6. Does the test case still run?

  7. Do we need database IDs? Can we not just use the strings as keys for these tables?

  8. If we try to assign() or permit() with missing roles/actions should these be created automatically? It would certainly make the client interface easier and we wouldn’t have to deal with so much error handling. The is an OO principle called “tell, don’t ask”. We just tell the admin. interface to add user’s and roles and not have to ask it whether they exist already.

yours, Marcus

Realistically, there is only a little bit left to implement. I was hoping that we could finish and get it working before we discuss the shortcomings of the solution. We only have RoleGateway, RoleFinder, PermissionGateway, and PermissionFinder left to code and then we can run your tests. Once we get it working in its current state we can iterate and refactor. What do you think about this approach?

Thanks,

JT

PearRoleGateway and PearRoleFinder implementations:


<?php
define('ROLE_TABLE', 'role');
define('ROLE_TABLE_NAME', 'name');
define('ROLE_TABLE_ID', 'id');

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(&$result) {
        $gateway =& new PearRoleGateway($this->_connection);
        $gateway->setRoleId($result['id']);
        $gateway->setName($result['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);
        return PearRoleGateway::load($result);
    }
}
?>

PearPermissionGateway and PearPermissionFinder implementations:


<?php
define('PERMISSION_TABLE', 'role');
define('PERMISSION_TABLE_NAME', 'name');
define('PERMISSION_TABLE_ID', 'id');

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 " . PERMISSION_TABLE . " (" . PERMISSION_TABLE_NAME . ") VALUES ('" . $this->_name . "')");
    }

    function update() {
        $this->_connection->query("UPDATE " . PERMISSION_TABLE . " SET " . PERMISSION_TABLE_NAME . "='" . $this->_name . "' WHERE " . PERMISSION_TABLE_ID . "=" . $this->_id);
    }

    function delete() {
        // delete by name
        $this->_connection->query("DELETE FROM " . ROLE_TABLE . " WHERE " . PERMISSION_TABLE_NAME . "='" . $this->_name . "'");
    }

    function &load(&$result) {
        $gateway =& new PearPermissionGateway($this->_connection);
        $gateway->setPermissionId($result['id']);
        $gateway->setName($result['name']);
        return $gateway;
    }
}

class PearPermissionFinder {
    var $_connection;

    function PearPermissionFinder(&$connection) {
        $this->_connection =& $connection;
    }

    function &findByName($name) {
        $sql = "SELECT * FROM " . PERMISSION_TABLE . " WHERE " . PERMISSION_TABLE_NAME . "='$name'";
        $result &= $this->_connection->query($sql);
        return PearPermissionGateway::load($result);
    }
}
?>

Okay. I believe those are the last 4 classes to implement before we can run the test cases. I can definitely see a lot of repetition. I think that this is actually good, it means that we have something to refactor and improve in our next iteration. I guess I will put all this code together and try to give it a go.

Thanks,

JT

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”:


&lt;?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-&gt;_storage = &$storage;
    }
    function &getPermissions($name) {
        $finder = &new PermissionsCache($this-&gt;_storage-&gt;createPermissionsFinder());
        return new Permissions($finder-&gt;findAllByName($name));
    }
    function &createEdit() {
        return $this-&gt;_storage-&gt;createEdit();
    }
}
?&gt;

In file “cache.php”:


&lt;?php
class PermissionsCache {
    var $_finder;

    function PermissionsCache(&$finder) {
        $this-&gt;_finder = &$finder;
    }
    function &findAllByName($name) {
        return $this-&gt;_finder-&gt;findAllByName($name);
    }
}
?&gt;

In file “pear_storage.php”:


&lt;?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-&gt;_connection = &$this-&gt;_createConnection($connection_string);
    }

    function createPermissionsFinder() {
        return new PearPermissionsFinder($this-&gt;_connection);
    }

    function &createEdit() {
        return new PearAuthorisationEdit($this-&gt;_connection);
    }

    function &_createConnection($connection_string) {
    	$db =& DB::connect($connection_string);
    	
    	if (DB::isError($db)) {
    		die($db-&gt;getMessage());
    	}

		return $db;
    }
}

class PearAuthorisationEdit {
    var $_connection;

    function PearAuthorisationEdit(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function addUsage($usage) {
        $gateway =& new PearUsageGateway($this-&gt;_connection);
        $gateway-&gt;setName($usage);
        $gateway-&gt;insert();
    }

    function dropUsage($usage) {
        $gateway =& new PearUsageGateway($this-&gt;_connection);
        $gateway-&gt;setName($usage);
        $gateway-&gt;delete();
    }

    function addRole($role) {
        $gateway =& new PearRoleGateway($this-&gt;_connection);
        $gateway-&gt;setName($role);
        $gateway-&gt;insert();
    }

    function dropRole($role) {
        $gateway =& new PearRoleGateway($this-&gt;_connection);
        $gateway-&gt;setName($role);
        $gateway-&gt;delete();
    }

    function addPermission($action) {
        $gateway =& new PearPermissionGateway($this-&gt;_connection);
        $gateway-&gt;setName($action);
        $gateway-&gt;insert();
    }

    function dropPermission($action) {
        $gateway =& new PearPermissionGateway($this-&gt;_connection);
        $gateway-&gt;setName($action);
        $gateway-&gt;delete();
    }

    // Usage Assignment: Assign Roles to Usages
    function assign($usage, $role) {
        // Row Data Gateway access for Usage table
        $finder =& new PearUsageFinder($this-&gt;_connection);
        $gateway =& $finder-&gt;findByName($usage);
        $usageId = $gateway-&gt;getUsageId();

        // Row Data Gateway access for Role Table
        $finder =& new PearRoleFinder($this-&gt;_connection);
        $gateway =& $finder-&gt;findByName($role);
        $roleId = $gateway-&gt;getRoleId();

        // Row Data Gateway access for Usage Assignment Table
        $gateway =& new PearUsageAssignmentGateway($this-&gt;_connection);
        $gateway-&gt;setUsageId($usageId);
        $gateway-&gt;setRoleId($roleId);
        $gateway-&gt;insert();
    }

    // Permission Assignment (PA): Assign Permissions to Roles
    function permit($role, $action) {
        // Row Data Gateway access for Role Table
        $finder =& new PearRoleFinder($this-&gt;_connection);
        $gateway =& $finder-&gt;findByName($role);
        $roleId = $gateway-&gt;getRoleId();

        // Row Data Gateway access for Permission Table
        $finder =& new PearPermissionFinder($this-&gt;_connection);
        $gateway =& $finder-&gt;findByName($action);
        $permissionId = $gateway-&gt;getPermissionId();

        // Row Data Gateway access for Permission Assignment Table
        $gateway =& new PearPermissionAssignmentGateway($this-&gt;_connection);
        $gateway-&gt;setRoleId($roleId);
        $gateway-&gt;setPermissionId($permissionId);
        $gateway-&gt;insert();
    }

    function commit() {
    	// ...
    }
}

// Data Mapper/Finder for Permissions object (Domain Object)
class PearPermissionsFinder {
    var $_connection;

    function PearPermissionsFinder(&$connection) {
        $this-&gt;_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-&gt;_connection-&gt;query($sql);

        if (DB::isError($result)) {
        	die($result-&gt;getMessage());
        }

        $all = array();
        while ($row =& $result-&gt;fetchRow(DB_FETCHMODE_ASSOC)) {
            $all[] = $row['permission'];
        }

        $result-&gt;free();

//      return new Permissions($all);
        return $all;
    }
}

class PearUsageAssignmentGateway {
    var $_connection;
    var $_roleId;
    var $_usageId;

    function PearUsageAssignmentGateway(&$connection, $roleId = 0, $usageId = 0) {
        $this-&gt;_connection =& $connection;
        $this-&gt;_roleId = $roleId;
        $this-&gt;_usageId = $usageId;
    }

    function setRoleId($roleId) {
        $this-&gt;_roleId = $roleId;
    }

    function setUsageId($usageId) {
        $this-&gt;_usageId = $usageId;
    }

    function insert() {
    	$sql = "INSERT INTO " . USAGE_ASSIGN_TABLE . " VALUES (" . $this-&gt;_usageId . ", " . $this-&gt;_roleId . ")";
    	
        $this-&gt;_connection-&gt;query($sql);

        if (DB::isError($this-&gt;_connection)) {
        	die($this-&gt;_connection-&gt;getMessage());
        }
    }

    function delete() {
    	$sql = "DELETE FROM " . USAGE_ASSIGN_TABLE . " WHERE " . USAGE_ASSIGN_TABLE_ROLE_ID . "=" . $this-&gt;_roleId . " AND " . USAGE_ASSIGN_TABLE_USAGE_ID . "=" . $this-&gt;_usageId;

        $this-&gt;_connection-&gt;query($sql);

        if (DB::isError($this-&gt;_connection)) {
        	die($this-&gt;_connection-&gt;getMessage());
        }
    }
}

class PearPermissionAssignmentGateway {
    var $_connection;
    var $_permissionId;
    var $_roleId;

    function PearPermissionAssignmentGateway(&$connection, $permissionId = 0, $roleId = 0) {
        $this-&gt;_connection =& $connection;
        $this-&gt;_permissionId = $permissionId;
        $this-&gt;_roleId = $roleId;
    }

    function setPermissionId($permissionId) {
        $this-&gt;_permissionId = $permissionId;
    }

    function setRoleId($roleId) {
        $this-&gt;_roleId = $roleId;
    }

    function insert() {
    	$sql = "INSERT INTO " . PERM_ASSIGN_TABLE . " VALUES (" . $this-&gt;_roleId . ", " . $this-&gt;_permissionId . ")";

        $this-&gt;_connection-&gt;query($sql);

        if (DB::isError($this-&gt;_connection)) {
        	die($this-&gt;_connection-&gt;getMessage());
        }
    }

    function delete() {
    	$sql = "DELETE FROM " . PERM_ASSIGN_TABLE . " WHERE " . PERM_ASSIGN_TABLE_PERM_ID . "=" . $this-&gt;_permissionId . " AND " . PERM_ASSIGN_TABLE_ROLE_ID . "=" . $this-&gt;_roleId;

        $this-&gt;_connection-&gt;query($sql);

        if (DB::isError($this-&gt;_connection)) {
        	die($this-&gt;_connection-&gt;getMessage());
        }
    }
}

class PearUsageGateway {
    var $_connection;
    var $_id;
    var $_name;

    function PearUsageGateway(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function setName($name) {
        $this-&gt;_name = $name;
    }

    function getName() {
        return $this-&gt;_name;
    }

    function setUsageId($id) {
        $this-&gt;_id = $id;
    }

    function getUsageId() {
        return $this-&gt;_id;
    }

    function insert() {
    	$sql = "INSERT INTO " . USAGE_TABLE . " (" . USAGE_TABLE_NAME . ") VALUES ('" . $this-&gt;_name . "')";
        $this-&gt;_connection-&gt;query($sql);
        if (DB::isError($this-&gt;_connection)) {
        	die($this-&gt;_connection-&gt;getMessage());
        }
    }

    function update() {
        $this-&gt;_connection-&gt;query("UPDATE " . USAGE_TABLE . " SET " . USAGE_TABLE_NAME . "='" . $this-&gt;_name . "' WHERE " . USAGE_TABLE_ID . "=" . $this-&gt;_id);
    }

    function delete() {
        // delete by name
        $this-&gt;_connection-&gt;query("DELETE FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_NAME . "='" . $this-&gt;_name . "'");
    }

    function &load(&$row) {
        $gateway =& new PearUsageGateway($this-&gt;_connection);
        $gateway-&gt;setUsageId($row['id']);
        $gateway-&gt;setName($row['name']);
        return $gateway;
    }
}

class PearUsageFinder {
    var $_connection;

    function PearUsageFinder(&$connection) {
        $this-&gt;_connection = $connection;
    }

    function &findByName($name) {
        $sql = "SELECT * FROM " . USAGE_TABLE . " WHERE " . USAGE_TABLE_NAME . "='$name'";
        $result =& $this-&gt;_connection-&gt;query($sql);
        $row =& $result-&gt;fetchRow(DB_FETCHMODE_ASSOC);
        return PearUsageGateway::load($row);
    }
}

class PearRoleGateway {
    var $_connection;
    var $_id;
    var $_name;

    function PearRoleGateway(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function setName($name) {
        $this-&gt;_name = $name;
    }

    function getName() {
        return $this-&gt;_name;
    }

    function setRoleId($id) {
        $this-&gt;_id = $id;
    }

    function getRoleId() {
        return $this-&gt;_id;
    }

    function insert() {
        $this-&gt;_connection-&gt;query("INSERT INTO " . ROLE_TABLE . " (" . ROLE_TABLE_NAME . ") VALUES ('" . $this-&gt;_name . "')");
    }

    function update() {
        $this-&gt;_connection-&gt;query("UPDATE " . ROLE_TABLE . " SET " . ROLE_TABLE_NAME . "='" . $this-&gt;_name . "' WHERE " . ROLE_TABLE_ID . "=" . $this-&gt;_id);
    }

    function delete() {
        // delete by name
        $this-&gt;_connection-&gt;query("DELETE FROM " . ROLE_TABLE . " WHERE " . ROLE_TABLE_NAME . "='" . $this-&gt;_name . "'");
    }

    function &load(&$row) {
        $gateway =& new PearRoleGateway($this-&gt;_connection);
        $gateway-&gt;setRoleId($row['id']);
        $gateway-&gt;setName($row['name']);
        return $gateway;
    }
}

class PearRoleFinder {
    var $_connection;

    function PearRoleFinder(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function &findByName($name) {
        $sql = "SELECT * FROM " . ROLE_TABLE . " WHERE " . ROLE_TABLE_NAME . "='$name'";
        $result =& $this-&gt;_connection-&gt;query($sql);
        $row =& $result-&gt;fetchRow(DB_FETCHMODE_ASSOC);
        return PearRoleGateway::load($row);
    }
}

class PearPermissionGateway {
    var $_connection;
    var $_id;
    var $_name;

    function PearPermissionGateway(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function setName($name) {
        $this-&gt;_name = $name;
    }

    function getName() {
        return $this-&gt;_name;
    }

    function setPermissionId($id) {
        $this-&gt;_id = $id;
    }

    function getPermissionId() {
        return $this-&gt;_id;
    }

    function insert() {
        $this-&gt;_connection-&gt;query("INSERT INTO " . PERM_TABLE . " (" . PERM_TABLE_NAME . ") VALUES ('" . $this-&gt;_name . "')");
    }

    function update() {
        $this-&gt;_connection-&gt;query("UPDATE " . PERM_TABLE . " SET " . PERM_TABLE_NAME . "='" . $this-&gt;_name . "' WHERE " . PERM_TABLE_ID . "=" . $this-&gt;_id);
    }

    function delete() {
        // delete by name
        $this-&gt;_connection-&gt;query("DELETE FROM " . PERM_TABLE . " WHERE " . PERM_TABLE_NAME . "='" . $this-&gt;_name . "'");
    }

    function &load(&$row) {
        $gateway =& new PearPermissionGateway($this-&gt;_connection);
        $gateway-&gt;setPermissionId($row['id']);
        $gateway-&gt;setName($row['name']);
        return $gateway;
    }
}

class PearPermissionFinder {
    var $_connection;

    function PearPermissionFinder(&$connection) {
        $this-&gt;_connection =& $connection;
    }

    function &findByName($name) {
        $sql = "SELECT * FROM " . PERM_TABLE . " WHERE " . PERM_TABLE_NAME . "='$name'";
        $result =& $this-&gt;_connection-&gt;query($sql);
        $row =& $result-&gt;fetchRow(DB_FETCHMODE_ASSOC);
        return PearPermissionGateway::load($row);
    }
}
?&gt;

In file “permissions.php”:


&lt;?php
class Permissions {
    var $_all;

    function Permissions(&$all) {
        $this-&gt;_all =& $all;
    }

    function can($action) {
        return in_array($action, $this-&gt;_all);
    }
}
?&gt;

In file “test.php”:


&lt;?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-&gt;UnitTestCase();
        $this-&gt;storage = &new PearAuthorisationStorage(CONNECTION_STRING);
    }
    function setUp() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $edit = &$authoriser-&gt;createEdit();
        $edit-&gt;addUsage('fred');
        $edit-&gt;addRole('pleb');
        $edit-&gt;addPermission('do_stuff');
        $edit-&gt;assign('fred', 'pleb');
        $edit-&gt;permit('pleb', 'do_stuff');
        $edit-&gt;commit();
    }

    function tearDown() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $edit = &$authoriser-&gt;createEdit();
        $edit-&gt;dropUsage('fred');
        $edit-&gt;dropRole('pleb');
        $edit-&gt;dropPermission('do_stuff');
        $edit-&gt;commit();
    }

    function testNonUserHasNothingAllowed() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $permissions = &$authoriser-&gt;getPermissions('public');
        $this-&gt;assertFalse($permissions-&gt;can('do_stuff'));
    }

    /*
    function testBadPasswordHasNothingAllowed() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $permissions = &$authoriser-&gt;getPermissions('fred');
        $this-&gt;assertFalse($permissions-&gt;can('do_stuff'));
    }
    */

    function testLegitimateUserHasActionAllowed() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $permissions = &$authoriser-&gt;getPermissions('fred');
        $this-&gt;assertTrue($permissions-&gt;can('do_stuff'));
    }

    function testUserCannotDoNonAction() {
        $authoriser = &new Authoriser($this-&gt;storage);
        $permissions = &$authoriser-&gt;getPermissions('fred');
        $this-&gt;assertFalse($permissions-&gt;can('do_unknown'));
    }
}

$test =& new RoleBasedPermissionsTest();
$test-&gt;run(new HtmlReporter());
?&gt;

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

Hi…

It is actually my prefered approach (tracer bullet). I just didn’t want to end up feeling guilty if the chosen refactoring ended up nuking all your code…:slight_smile:

yours, Marcus

p.s. This reminds me of pair programming at work. One person “drives” (types) whilst the other muses over the design and act as the brake.

Does anyone know what DB seratonin is using?

mysql, see CONNECTION_STRING in test.php


define('CONNECTION_STRING', 'mysql://root:@localhost/test');

I guess that you have problem with fetchRow. I had :slight_smile:
provided sql schema is using ‘usage’ as table name and since usage is reserved word table cannot be created. Change USAGE_TABLE constant in pear_storage.php to new tbl name.

[offtopic]

Change USAGE_TABLE constant
If you have to change a constant, it doesn’t really have any business being a constant, right? :)[/offtopic]

I had to make a minor change to the database schema. I left out the fact that together the two id’s make the primary key for both connective tables (usage_assign and perm_assign). Also, regarding the reserved word “usage”. I ran into the problem of MySQL not liking it so I ended up putting “`” marks around it and got it to work. The best idea, I think, is to just change the name of the table all together.

Thanks,

JT