PSR-7 Permissions

One of the hardest things to do in a web application is to lock certain users to certain functions. Anyone who has tried to DIY their own ACL list or permission structure can attest to it becoming a giant nightmare because Sally in accounting needs this report and that report, but not these other 5, but maybe in the future she might. *LE SIGH*

So to save all of you a lot of time and frustration, I bring to you an easy solution. https://github.com/geggleto/geggleto-acl

ACL

  • Access
  • Control
  • List

So let's talk about how we can do it with this package.

The Basics

There are a few concepts we need to cover before we dive into some coding. There are some very important terms to get out of the way. This package is based upon Zend Frameworks ACL and provides an easy to use Array config and PSR-7 middleware to use the Acl with.

Resources - These are the end points which you want to secure.
Roles - These are labels for different access levels. I choose to make these labels mean one thing only, 1 resource.

Config

//Define or Pull your ACL's into the following format
$config = [
    "resources" => ["/", "/no", "/yes"],
    "roles" => ["guest", "user1", "user2"],
    "assignments" => [
        "allow" => [
            "guest" => ["/"],
            "user1" => ["/", "/no"],
            "user2" => ["/", "/yes"]
        ],
        "deny" => [
            "guest" => ["/no", "/yes"],
            "user1" => ["/yes"],
            "user2" => ["/no"]
        ]
    ]
];

Explanation

In this example we are defining 3 "pages" and 3 "roles". We can see here each "user" has their own set of allowable pages. This is one way to mange your users. It should be noted that by default our middleware will deny anything that is not in the allowable section, so the deny is not required. But for maximum readability it can be nice to put them in anyway.

Example Usage

//In Slim v3
$app->add(\Geggleto\Acl\AclRepository(["guest"], 
//This should be in a nice php file by itself for easy inclusion... include '/path/to/acl/definition.php'
[
    "resources" => ["/", "/no", "/yes"],
    "roles" => ["guest", "user1", "user2"],
    "assignments" => [
        "allow" => [
            "guest" => ["/"],
            "user1" => ["/", "/no"],
            "user2" => ["/", "/yes"]
        ],
        "deny" => [
            "guest" => ["/no", "/yes"],
            "user1" => ["/yes"],
            "user2" => ["/no"]
        ]
    ]
]));

Explanation

In this example we are simply using our sample config, and adding it to our framework Slim v3. The AclRepository class is Invokable so no magic is required to get this working in your Slim v3 prject.

Dynamic Routes

So far we have had some very simple and straight forward routes to protect. However we do need to account for route patterns that have wildcards in it. AclRepository's middleware checks the actual route pattern of an object if a basic route match fails. That is to say that if we were to have a route "/roles/{id}" we can check it.

Let's take a look at another sample config this time with a bit more difficult route.

 [
    "resources" => ["/", "/login", "/grid", "/404", "/logout", "/roles", "/roles/{pein}"],
    "roles" => ["guest", "grid", "roles"],
    "assignments" => [
        "allow" => [
            "guest" => ["/", "/404", "/login"],
            "grid" => [ '/grid', '/logout' ],
            "roles" => ['/roles', '/roles/{pein}']
        ],
        "deny" => []
    ]
];

In this config above you can see we are defining a route /roles/{pein} . This matches a route in our application with the same pattern. We can see that someone with the Roles permission can access it. This works out of the box with AclRepository when you are using Slim v3. The only thing required is that you set 'determineRouteBeforeAppMiddleware' = true. You can see the Slim documentaton on how to do that, it's quite literally a one-line change.

This change in slim set's the actual route object into the Request object where AclRepsitory can call getPattern() to match against.

If you are not using slim 3... read on my friend.

So your not using Slim 3... that's okay I got your back

Let's take a look at the actual middleware code that AclRepository ships with.


$app->add(function (Request $request, Response $res, $next) {
    /** @var $aclRepo AclRepository */ 
    $aclRepo = $this->get(AclRepository::class); //In Slim 3 the container is bound to function definitions
    $allowed = false; // We assume that the user cannot access the route

    $route = '/' . ltrim($request->getUri()->getPath(), '/'); //We construct our path

    try { //Check here... This will pass when a route is simple and there is no route parameters
        $allowed = $aclRepo->isAllowedWithRoles($aclRepo->getRole(), $route);
    } catch (InvalidArgumentException $iae) { //This is executed in cases where there is a route parameters... /user/{id:} 
        $fn = function (ServerRequestInterface $requestInterface, AclRepository $aclRepo) {
            //This will likely only work in Slim 3... This requires the determineRouteBeforeAppMiddleware => true to be set in the container
            $route = $requestInterface->getAttribute('route'); // Grab the route to get the pattern
            if (!empty($route)) {
                foreach ($aclRepo->getRole() as $role) {
                    if ($aclRepo->isAllowed($role, $route->getPattern())) { // check to see fi the user can access the pattern
                        return true; //Is allowed
                    }
                }
            }
            return false;
        };

        $allowed = $fn($request, $aclRepo); // Execute the fail-safe
    }

    if ($allowed) {
        return $next($request, $res);
    } else {
        return $res->withStatus(401); //Is not allowed. if you need to render a template then do that.
    }
});

Yeah okay, I get it. There's a lot of stuff going on and this might not make a lot of sense. Please bear with me. It's all quite simple.

What you are really going to want to do, is redefine $fn. You will want to find a way to get the Pattern of the route that was being executed. Other frameworks probably do this differently, and I happen to use Slim 3 at work, so please do feel free to PR the repo when you find a new way to work this into different frameworks.

$fn's job is to evaluate the request for a non-standard route ie a dynamic route. It can be broken down into a few simple steps.

  1. Get the current route object. Ie the Route object that is bein dispatched
  2. Get the route pattern
  3. Test it using $aclRepo->isAllowed($role, $pattern)
  4. Return true or false...
  5. Profit.

Seriously that is it. I know it looks super scary... but it really is pretty straight forward.

How I do it

Some people call be "different", I have been around for a few years and what I have realized is that requirements are great but things change, and when they do managers want results quickly. As I briefly touched on earlier, I prefer the most absolutely insane configuration for production when it comes to assigning roles.

Literally every route is assignable.

*gasp* I know it seems crazy, but trust me people overkill is highly underated. If I had a dollar for every time I get a phone call on a vacation because Joe is off on vacation so now Bob needs this one report, and he MUST NOT have the other stuff Joe has. Ugh F*** that. Every route is assignable. Every Route is a role. Every role gets assigned one route. Some people call it crazy, I call it lazy.

I hope you enjoyed this article briefly describing my package, if you like it please feel free to tweet @ me.

o7 Happy Coding

Written by Glenn Eggleton on Tuesday January 26, 2016
Permalink - Chapter: php