sitepoint-editors / Rauth

A basic annotation-based ACL package for PHP
MIT License
41 stars 4 forks source link

Rauth

Latest Version on Packagist Software License Build Status Coverage Status Quality Score

Rauth is a simple package for parsing the @auth-* lines of a docblock. These are then matched with some arbitrary attributes like "groups" or "permissions" or anything else you choose. See basic usage below.

Why?

I wanted:

Annotations Are Bad ™

Somewhat "controversially", Rauth defaults to using annotations to control access. No matter which camp you're in regarding annotations in PHP, here's why their use in Rauth's case is nowhere near as wrong as some make it out to be:

Install

Via Composer

composer require sitepoint/rauth

Basic Usage

Boostrap Rauth somewhere (preferably a bootstrap file, or wherever you configure your DI container) like so:

<?php

$rauth = new Rauth();

Note: you can use setCache or the Rauth constructor to inject a Cache object, too. Default to ArrayCache (so, inefficient and doesn't really cache anything), but can be replaced by anything that follows the Cache interface. See src/Rauth/Cache.php.

Define requirements in @auth-* lines of a class or method docblock:

<?php

namespace Paranoia;

/**
 * Class MyProtectedClass
 * @package Paranoia
 *
 * @auth-groups admin, reg-user
 * @auth-permissions post-write, post-read
 * @auth-mode OR
 *
 */
class MyProtectedClass
{

"Groups" and "Permissions" are arbitrary attributes a user can have - you can use "bananas" or "squirrel-hammocks" if you want. All that matters is that you separate their values with commas and that the tag starts with @auth-.

To check if a user has access to a given class / method:

try {
    $allowed = $rauth->authorize($classInstanceOrName, $methodName, $attributes);
} catch (\SitePoint\Rauth\Exception\AuthException $e) {
    $e->getType(); // will be "ban", "and", "or", etc...
    $e->getReasons(); // an array of Reason objects with details
}

$attributes will be an array you build - this depends entirely on your implementation of user attributes. Maybe you're using something like Gatekeeper and have immediate access to groups and/or permissions on a User entity, and maybe you have a totally custom system. What matters is that you build an array which contains the attributes like so:

$attributes = [
    'groups' => ['admin']
];

or maybe something like this:

$attributes = [
    'permissions' => ['post-write', 'post-read']
];

or even something like this:

$attributes = [
    'groups' => ['admin', 'reg-user'],
    'permissions' => ['post-write', 'post-read']
];

You get the drift.

Remember: the @auth-* lines are requirements, and they are compared against attributes

Rauth will then parse the @auth lines and save the attributes required in an array similar to that, like so:

$requirements = [
    'mode' => RAUTH::OR,
    'groups' => ['admin', 'reg-user'],
    'permissions' => ['post-write', 'post-read']
];

authorize will return true if all is well.

If the authorize check fails, it will throw an AuthException. The AuthException will have a getType getter which will return a string value of the mode in which the failure happened - be it ban, and, or, none, or a custom mode altogether (see modes below). It will also have a getReasons getter which provides an array of Reason objects. Each object has the following public properties:

Available Modes

These modes can be used as values for @auth-mode:

OR

The mode OR will make Rauth::authorize() return true if any of the attributes matches any of the requirements.

AND

The mode AND will make Rauth::authorize() return true if all of the attributes match all of the requirements (e.g. user must have ALL the groups and ALL the permissions and ALL the bananas mentioned in the docblock).

NONE

The mode NONE will make Rauth::authorize() return true only if none of the attributes match the requirements.

Ban

Another option you can use is the @auth-ban tag:

/*
* ...
* @auth-ban-groups guest, blocked
* ...
*/

This tag will take precedence if a match is found. So in the example above - if a user is an admin, but is a member of the blocked group, they will be denied access. All ban matches MUST be zero if the user is to proceed, regardless of all other matches.

The banhammer wields absolute authority and does not react to @auth-mode. Bans must be completely cleared before other permissions are even to be looked at.

Caching

Rauth accepts in its constructor a Cache object which needs to adhere to the src/Rauth/Cache.php interface. It defaults to ArrayCache, which is a fake cache that doesn't really improve speed by any margin and is mainly used during development.

Note that you can pass in a ready-made array into the ArrayCache (constructor accepts data), if you have it. This way, you'd hydrate the cache for Rauth and it wouldn't have to manually parse every class it tries to authorize:

$ac = new ArrayCache(
    [
        'SomeClass' => [
            'mode' => RAUTH::OR,
            'groups' => ['admin', 'reg-user'],
            'permissions' => ['post-write', 'post-read'],
        ],
        'SomeClass::someMethod' => [
            'mode' => RAUTH::AND,
            'groups' => ['admin'],
        ],
    ]
);

$rauth = new Rauth($ac);

Best Practice

In order to avoid having to use the authorize call manually, it's best to tie it into a Dependency Injection container or a route dispatcher. That way, you can easily put your requirements into the docblocks of a controller, and build the attributes at bootstrapping time, and everything else will be automatic. For an example of this, see the nofw skeleton.

@todo This example will be added soon

Testing

composer test

Contributing

Please see CONTRIBUTING.

Credits

License

The MIT License (MIT). Please see License File for more information.