Patterns Tutorial Series (part 1): RBAC Domain Model

Marcus,

The more I look at your design, the more I like it. :wink:

What about separating the Authentication from the Authorization? It would be nice to just use the Authentication component only if all I wanted to do was password protect something and didnā€™t want to mess with a permission set.

Also, what about telling the authenticator and/or authorizor that we would like to cache the permission set somewhere (e.g. Session)?

Thanks,

JT

Hiā€¦

Cool, but itā€™s pure evolution and missing details. I think it is time to commit to some to code and achieve an iteration.

I think take it one step at a time. Letā€™s do the Authorisation first, and then add this if it seems the right thing to do in the light of day. At the moment the end point of the design is unclear to me to say the least.

I am afraid I have no idea right now, itā€™s just too fuzzy. Are you familiar with MockObjects? We can build the top level without committing to persistence if you are.

yours, Marcus

I donā€™t want to spoil the party, but I think that you took a far too big project to start with. I even think a project of such a size canā€™t be well managed in just a single forum thread. This project may need a whole forum for itā€™s ownā€¦

I must say I like the idea of collaborating on a coding project to learn design and coding techniques. Iā€™d like to participate if there was some more structure in the organizationā€¦

I think the best way to do something like this is start a new Sourceforge project. Sourceforge has a forum, version management, etc, just what you need. Ofcourse, it must be made clear to visitors that the main purpose for development is learning, not delivering production quality code.

Iā€™ll publish my implementation of a RBAC, if anyone is interested. I has intergrated workflow as well. The only problem is itā€™s coupled to my framework, so. Unless I use pear it may not FIT out of box into someone elseā€™s implementation.

ā€¦or exmples of implementation if thatā€™s easier.

Hiā€¦

Maybe. I donā€™t think that is the case as long as we avoid creeping featuritis. A single implementation (MySQL say) should only be a dozen small classes top. That is 150 lines of code total plust tests. If the project is smaller than this then nothing really gets illustrated.

This whole thread is an experiment of course, and like all early efforts has a high risk of failure. I am happy to take a chance and it is going better than I expected already.

I agree and was going to suggest this later. I think the discussion should take place here with example snippets to explain things. I like the idea of leaving behind a tutorial on SP with the code on SF.

A single SourceForge Project called ā€œsitepointphpā€ (copyright issues - can we get permission?) or some such with one module per project would be fine. You can then explain in the project blurb that it is tutorial material and SP should be searched for the real meat.

Sourceforge will be needed just so that fully fleshed out code and test cases can be placed somewhere, but it also adds version control. It is supposed to be a best practices thread after all :).

yours, Marcus

I donā€™t think that the endevour is too large now that it has been scoped. As Lastcraft mentioned, it shouldnā€™t be more than a dozen classes and a couple hundred lines of code.

About the Source Forge idea, I think that if we want to release code to the community then SF is a good idea. Here I think all we need to do is flesh out some of the classes in order to illustrate the patterns used and justify the choice of those patterns. We can also use this facility to ellicit feedback from other developers and hopefully go through several iterations. I guess the question comes down to: ā€œDo we want to show and explain some of the code here or do the entire implementation and release it?ā€

JT

On the point of BinaryCloud, after looking at the User, Perm and Auth classes, I noticed that these are accessed via Singletons, yes ?

In my view, for the purposes of these classes, the Singleton is a bad idea - there is no real need to return only the one instance of these classes, whereby a better idea would be to use :: instead, where the class itā€™s self isnā€™t instantiated.

Just seams over the top that all three classes have Singletons really - also the classes are mixing business logic (?) with SQL as well, which is breaking layering.

Whatā€™d other members think ? :slight_smile:

I agree with you. It looks like most of the BinaryCloud components are accessed via Singletons. It would be interested to ask the BC developers what the rationale was for that. Having SQL in there also smells bad. I would probably use a TableGateway pattern at least.

JT

I have done extensive work in this area. Is it ok if I help, as I have no wish or time to manage and work a project on my lonesome?

Anyhow, this thread turned out more informative than i expected. i thought it was just a question in general, seems it is an effort to achieve! Awesome.

If I can help, I would like too.

thanks

Hiā€¦

Of course :).

Still trying to define the interface correctly, here is a possible acceptance test with the authentication stripped out. I have called it tests/acceptance_tests.php. Edit to taste (switch it to PHPUnit if you like)ā€¦


<?php
require_once('../authoriser.php');
require_once('../access_controller.php');
require_once('../authoriser_options.php');
AuthoriserOptions::setConfigurationFile('authoriser.conf');

class RoleBasedPermissionsTest extends UnitTestCase {
    function RoleBasedPermissionsTest() {
        $this->UnitTestCase();
    }
    function setUp() {
        $authoriser = &new Authoriser();
        $authoriser->addUsage('fred');
        $authoriser->addRole('pleb');
        $authoriser->addOperation('do_stuff');
        $authoriser->attachRole('fred', 'pleb');
        $authoriser->permit('pleb', 'do_stuff');
    }
    function tearDown() {
        $authoriser = &new Authoriser();
        $authoriser->dropUsage('fred');
        $authoriser->dropRole('pleb');
        $authoriser->dropOperation('do_stuff');
    }
    function testNonUserHasNothingAllowed() {
        $access_controller = &new AccessController();
        $permissions = &$access_controller->getPermissions('public');
        $this->assertFalse($permissions->can('do_stuff'));
    }
    function testBadPasswordHasNothingAllowed() {
        $access_controller = &new AccessController();
        $permissions = &$access_controller->getPermissions('fred');
        $this->assertFalse($permissions->can('do_stuff'));
    }
    function testLegitimateUserHasActionAllowed() {
        $access_controller = &new AccessController();
        $permissions = &$access_controller->getPermissions('fred');
        $this->assertTrue($permissions->can('do_stuff'));
    }
    function testUserCannotDoNonAction() {
        $access_controller = &new AccessController();
        $permissions = &$access_controller->getPermissions('fred');
        $this->assertFalse($permissions->can('do_unknown'));
    }
}
?>

I have split Authorisor from AccessController so that the whole of the library doesnā€™t have to be loaded just to check someoneā€™s permissions. If someone has an alternate scheme it would simplify the interface slightly to have one object instead of two.

Here is a sample test runnerā€¦


<?php
define('SIMPLE_TEST', '/var/www/html/simpletest/');
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');

$test = &new GroupTest('Sitepoint advPHP RBAC');
$test->addTestFile('acceptance_tests.php');
$test->run(new HtmlReporter());
?>

This obviously only works on my machine.

To get this test so that it doesnā€™t crash I have to create the files. First authoriser.phpā€¦


<?php
class Authoriser {
    function Authoriser() {
    }
    function addUsage() {
    }
    function dropUsage() {
    }
    function addRole() {
    }
    function dropRole() {
    }
    function addOperation() {
    }
    function dropOperation() {
    }
    function attachRole() {
    }
    function permit() {
    }
}
?

Then access_controller.phpā€¦


<?php
class AccessController {
    function AccessController() {
    }
    function getPermissions() {
        return new Permissions();  // Blatant fake
    }
}

class Permissions {
    function Permissions() {
    }
    function can() {
    }
}
?>

And finally authoriser_options.phpā€¦


<?php
class AuthoriserOptions {
    function setConfigurationFile() {
    }
}
?>

With this set up I get just one failure. This is just about the minimum amount of code I can write for RBAC to make sense. It should work as a starting skeleton at least.

yours, Marcus

Thanks Marcus, as always you have something usefull to contribute :slight_smile:

Nothing wrong with this part though, looks clean enough IMO.


...
$authoriser = &new Authoriser();
        $authoriser->addUsage('fred');
        $authoriser->addRole('pleb');
        $authoriser->addOperation('do_stuff');
        $authoriser->attachRole('fred', 'pleb');
        $authoriser->permit('pleb', 'do_stuff');
...

Makes sense really :slight_smile:

If I can help, I would like too.

The more, the merrier :wink:

Itā€™s looking good, Marcus. Here are my comments:

  1. Why did you decide to separate out the AccessController from the Authorizer?

  2. Naming - what is the difference between an operation and a permission? I think the naming should be consistent between the Authorizer class and the Permissions class. So if we go with just ā€œpermissionā€, I think instead of addOperation/dropOperation, I would go with grantPermission, revokePermission. This is just a matter of preference, I guess.

Thanks,

JT

Hiā€¦

This is possibly a premature optimisation. As the most common operation is checking a permissions set, rather than modifying the permissions map, it seemed to make sense to keep this part of the code lean and mean. Otherwise you could end up with a lot of unecessary require_once()'s for the most common operation. I may not have chosen the best approach though. It would be nice to have just one class.

Good point. Permissions is a releationship and ā€œpermitā€ is a transient act, hence why I chose a different name. Permissions is obviously correct in the access control interface and it does make it more consistent to do it the same way in the admin. interface. Please repost your modifications.

I see I have chosen English (UK) spellings without thinking as well. Itā€™s the colonial past catching up with me :).

yours, Marcus

p.s. Seratonin: As you have access to the first post in the script, do you fancy updating it as a summary of what is happening as we go?

That makes sense. Since updating permissions happens much less frequently than retreiving permissions, the separation is a good idea.

I think that Authorizer is a deceiving name anyways for the Admin interface. Something like PermissionsAdmin, RbacAdmin, or AccessControlAdmin might be a better name since itā€™s sole reponsiblity is the administrative functionality. Maybe we have two classes AccessController (responsible for controlling access) and AccessAdministrator (responsible for administrating access).

I will update the first post with a summary of the topic. We may want to start a (part 2) to this forum so that it doesnā€™t become too cluttered.

JT

Hiā€¦

Or Authoriser and AuthorisationEditor respectively. In fact I think these are the ā€œrightā€ names. I think these names are more precise than names with controller and manager in. They always make me uneasy.

Is this a good idea? Having to jump from post to post is going to cause chaos, especially if someone posts something in a previous part causing a fork. I havenā€™t had a problem with the long post yet, and I think it is good to tell the whole story. There may be a bigger problem with digressions though.

On testing: I have posted the acceptance test because I think it is a good way of nailing down the spec. Iā€™ll avoid posting every unit test if I can though, as that will double LOC.

The next bit should be the actual Authoriser::getPermissions() method and making the editor transactional. These are the next big issues I think. Anyway - so far we are here, yesā€¦?


class RoleBasedPermissionsTest extends UnitTestCase {
    function RoleBasedPermissionsTest() {
        $this->UnitTestCase();
    }
    function setUp() {
        $editor = &new AuthorisionEditor();
        $editor->addUsage('fred');
        $editor->addRole('pleb');
        $editor->addPermission('do_stuff');
        $editor->attachRole('fred', 'pleb');
        $editor->permit('pleb', 'do_stuff');
        $editor->commit();
    }
    function tearDown() {
        $editor = &new AuthorisionEditor();
        $editor->dropUsage('fred');
        $editor->dropRole('pleb');
        $editor->dropPermission('do_stuff');
        $editor->commit();
    }
    function testNonUserHasNothingAllowed() {
        $authoriser = &new Authoriser();
        $permissions = &$authoriser->getPermissions('public');
        $this->assertFalse($permissions->can('do_stuff'));
    }
    function testBadPasswordHasNothingAllowed() {
        $authoriser = &new Authoriser();
        $permissions = &$authoriser->getPermissions('fred');
        $this->assertFalse($permissions->can('do_stuff'));
    }
    function testLegitimateUserHasActionAllowed() {
        $authoriser = &new Authoriser();
        $permissions = &$authoriser->getPermissions('fred');
        $this->assertTrue($permissions->can('do_stuff'));
    }
    function testUserCannotDoNonAction() {
        $authoriser = &new Authoriser();
        $permissions = &$authoriser->getPermissions('fred');
        $this->assertFalse($permissions->can('do_unknown'));
    }
}

yours, Marcus

I will update the first message in this topic and we will continue as we are.

Thanks,

JT

Hiā€¦

Just to get some grunt work out of teh way, here is options.php. Should be all the Singleton/global data we ever need.


<?php
class AuthoriserOptions {

    function setConfigurationFile($file) {
        $registry = &AuthoriserOptions::_getRegistry();
        $registry['file'] = $file;
    }

    function getConfigurationFile() {
        $registry = &AuthoriserOptions::_getRegistry();
        return $registry['file'];
    }

    function setConfigurationClass($class) {
        $registry = &AuthoriserOptions::_getRegistry();
        $registry['class'] = $class;
    }

    function getConfigurationClass() {
        $registry = &AuthoriserOptions::_getRegistry();
        return $registry['class'];
    }

    function &_getRegistry() {
        static $registry = false;
        if (! $registry) {
            $registry = array(
                    'file' => false,
                    'class' => 'AuthoriserConfiguration');
        }
        return $registry;
    }
}
?>

yours, Marcus

Now Iā€™m lost as to why you need this class ?? :confused:

Hiā€¦

Excellent question :). I have gone completely mad. Basically I was trying to get this to workā€¦


<?php
class AuthorisationEditor {
    var $_storage;

    function AuthorisationEditor() {
        $configuration_class =
                AuthorisationOptions::getConfigurationClass();
        $configuration = &new $configuration_class(
                AuthorisationOptions::getConfigurationFile());
        $this->_storage = &$this->_createStorage($configuration);
    }
    function &_createStorage($configuration) {
        $storage_class = $configuration->get('storage_class');
        return new $storage_class();
    }
    function addUsage() {
    }
    ...
}
?>

There are three hooks in this. The aim is to get the appropriate storage factory set up. This needs a configuration file if itā€™s shipped as part of a package. In order to run the acceptance tests and to make things configurable, the name of this file needs to be changed. That is number one. If the library is extended then the configuration class would have to extended too and thatā€™s two. The last hook is the protected factory method for swapping the storage for testing. All to avoid passing stuff in the constructor.

I now think it is way too complicated and a generally bad idea :(.

Assume that we need a storage driver class for a moment (even this is probably wrong). If we allow constructor parameters for the AuthorisationEditor() then a configuration file is not needed at all. The catch is that the AuthorisationEditor and the Authoriser must get to use the same storage mechanism. If we place this responsibility on the application then changes to one may not get rippled through. There are several much better solutions to this.

  1. Pass in the configuration object/hash. The application probably already has one anyway.
  2. Have the application create the storage class anyway.
  3. Have the editor created from a factory in the Authoriser.

No need for options stuff at all either way :).

Trouble is I am now having problems with the design above :(. It is really hard to add code to the editor that is not SQL dependent without moving everything into the storage class (which just moves the problem). To be flexible I think the solution is to have different editors (which are basically data accessors in this data centric problem).

Abandoning the class separation (which now seems to cause way to many complications)ā€¦


<?php
require_once('../authoriser.php');
require_once('../configuration.php');

class RoleBasedPermissionsTest extends UnitTestCase {
    function RoleBasedPermissionsTest() {
        $this->UnitTestCase();
        $this->_configuration = &new AuthorisationConfiguration();
    }
    function setUp() {
        $authoriser = &new Authoriser($this->_configuration);
        $edit = &$authorisor->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->_configuration);
        $edit = &$authorisor->createEdit();
        $edit->dropUsage('fred');
        $edit->dropRole('pleb');
        $edit->dropPermission('do_stuff');
        $edit->commit();
    }
    function testNonUserHasNothingAllowed() {
        $authoriser = &new Authoriser($this->_configuration);
        $permissions = &$authoriser->getPermissions('public');
        $this->assertFalse($permissions->can('do_stuff'));
    }
    ...
}
?>

This is much simpler. Now the storage is created within the Authoriser only (from a class name) and in turn it has to create the PermissionsFinder and AuthorisationAccessor (basically the edit).

Still thrashing, but I feel I am getting somewhere. What have I done wrong now ;)?

yours, Marcus

We may want to start a (part 2) to this forum so that it doesnā€™t become too cluttered.

If you can think of a catchy title to go with it, then even better :wink: I think this thread is already cluttered myself, so another thread would be great, just to point out as to where the project is at the moment ?

If someone could then post what script has been done thus far ?