Skip to content
Michael Spiss edited this page Mar 25, 2017 · 6 revisions

Solution10\Auth contains a flexible and powerful permissions model.

You can define broad access control groups called "Packages" and for mis-behaving users you can also override specific permissions.

But more on that later.

Contents

Defining Access with Packages

Let's have a look at a basic Package:

use Solution10\Auth\Package;

class DefaultPackage extends Package
{
    public function init()
    {
        $this
            ->precedence(1)

            // Set individually:
            ->permission('login', true)

            // Or as a massive array:
            ->permissions(
                array(
                    'postTopics'    => true,
                    'postStickies'  => false,
                    'editPost' => array($this, 'editPost'),
                    'privateMessage'    => __NAMESPACE__ . '\Package::privateMessage',
                    'lockTopic'         => function ($user, $topic) {
                        return $thisUser->isAdmin();
                    }
                )
            );
    }

    public function name()
    {
        return 'DefaultPackage';
    }

    public function editPost(UserRepresentation $user, $post)
    {
        // You could check that user->id matches the post->user->id but I'm mean:
        return false;
    }

    public static function privateMessage(UserRepresentation $user)
    {
        // msg me plz
        return true;
    }
}

Phew, there's kind of a lot there! Let's walk through it step by step:

The init() function

init() is where a package defines it's permissions. You can define permissions in one of two ways: rules and callbacks. It also defines Precedence.

Rules

Rules are straight yes/no decisions. In our above Package, a user with this package can login, they can postTopics but they cannot postStickies.

Callbacks

Callbacks are for permission decisions that are contextual, or need more info to make the decision. Take editing a post on a forum. Only the person who wrote the post should be able to edit it, so you could pass in the post you want to check.

Anything PHP considers 'callable' can be used as a Callback.

Callbacks always get the user we're checking for as their first argument (notice the UserRepresentation type hints on editPost() and privateMessage() above?). Anything else you pass gets handed as-is to the callback!

Your callbacks must return a boolean. They must not throw exceptions.

Managing the Packages on a User

Sweet, so we have our awesome package, how do we add it to a user?

Like so:

$user = new UserImplementingUserRepresentation();
$defaultPackage = new DefaultPackage();

$auth->addPackageToUser($user, $defaultPackage);

What about removing packages?

$user = new UserImplementingUserRepresentation();
$defaultPackage = new DefaultPackage();

$auth->removePackageFromUser($user, $package);

And you can also easily grab information on what packages a user has like so:

$user = new UserImplementingUserRepresentation();

if ($auth->hasPackage(new AdminPackage()) || $auth->hasPackage('Moderator')) {
    echo 'Hello Staff Member!';
}

$allPackages = $auth->packagesForUser($user);

echo 'You are a...<br>';
foreach ($packages as $package) {
    echo $package->name().'<br>';
}

Asking for Permission

So now we have a user with the DefaultPackage. How can we check that they can do stuff?

if ($auth->can('login')) {
    echo 'Yep, the current user can login!';
}

The can() function uses the name you defined in init() to check the permissions of the user who is currently signed into this Auth instance.

You can also check the permission of any user with userCan():

$user = new UserImplementingUserRepresentation(27);

if ($auth->userCan($user, 'login')) {
    echo 'User 27 can login too!';
}

Using callbacks is done in the same way, just pass an array of args afterwards:

if ($auth->can('editPost', [$post]) {
    echo 'User can edit this post';
}

$user = new UserImplementingUserRepresentation(27);
if ($auth->userCan($user, 'privateMessage') === false) {
    echo 'User 27 can\'t send private messages.';
}

Multiple Packages (Precedence)

That explains the rules and callbacks, but what is that Precedence function?

Well, Users can have multiple packages attached to them, and Packages can overwrite each others access control.

Consider the following:

class BasicPackage extends Package
{
    public function init()
    {
        $this
            ->precedence(1)
            ->permission('login', true)
            ->permission('lockTopics', false);
    }
}

class AdminPackage extends Package
{
    public function init()
    {
        $this
            ->precedence(10)
            ->permission('lockTopics', true);
    }
}

$user = new UserImplementingUserRepresentation();
$auth->addPackageToUser($user, new BasicPackage());
$auth->addPackageToUser($user, new AdminPackage());

Precedence allows us to do a form on inheritance between packages without needing to subclass.

The AdminPackage has a higher precedence and so it over-writes the lockTopics permission to be true, therefore User 1 can 'lockTopics'.

Since AdminPackage doesn't change the 'login' permission, it bubbles up from the BasicPackage, meaning that User 1 can 'login'.

Design your packages so no two have the same precedence!

Rules and callbacks can both be over-written and they can "jump type" as well. What we mean by that is what was a callback on a lower package can become a rule on a higher package.

This sounds confusing, so let's see an example:

class BasicPackage extends Package
{
    public function init()
    {
        $this
            ->precedence(1)
            ->permission('editPost', function (UserRepresentation $user, $post) {
                return $user->id() === $post->user->id();
            });
    }
}

class AdminPackage extends Package
{
    public function init()
    {
        $this
            ->precedence(10)
            ->permission('editPost', true);
    }
}

As you can see, the editPost permission has gone from being a tricky decision about post ownership when a lowly user asks for it, to a dead simple YES when an admin asks for it.

This behaviour means you can and should keep permission callbacks super simple and over-write from packages higher up the chain.

See the API docs for more information on Packages, they're heavily documented and tested.

Overrides

Packages are great and all, but sometimes you have problem users. Those numptys who just need a banhammer to the face for a while. How are you supposed to do this with broad access control like Packages?

Well, you could create a MoronPackage class with the good bits turned off, but that's not granular enough. Maybe you just want to stop them posting for a bit.

Enter; Overrides.

Let's say I'm using that DefaultPackage from the first section on this page, but User 27 is being a moron and spamming. I can disable just his postTopics permission like so:

$user = new UserImplementingUserRepresentation(27);
$auth->overridePermissionforUser($user, 'postTopics', false);

And boom, User 27 now can't postTopics. Everyone else with DefaultPackage can continue to postTopic's it's just User 27 that's affected.

At the end of their ban, you can remove the override like so:

$user = new UserImplementingUserRepresentation(27);
$auth->removeOverrideForUser($user, 'postTopics');

If you find you've set a lot of overrides onto a user and want to clear them all, you can do the following:

$user = new UserImplementingUserRepresentation(27);
$auth->resetOverridesForUser($user);

Where do Overrides sit in Precedence?

Overrides always sit at the top of the inheritance tree. You can't reset an override by adding more Packages to a user, only removing or resetting the Overrides will do that.