As a part of a new project I’m working on (personal, not work) I came across a common need to enforce authentication and authorization handling in a bit more automated way based on the URL requested. I looked around for options and didn’t really find many that could be implemented somewhat simply but I did like the way Symfony defines their YAML to enforce auth* on the various endpoints. I set out to make something similar but a little simpler and ended up making Invoke.
It’s a super simplified version of the YAML-based routing and only has functionality for checking groups and permissions right now, but that’s not what I really wanted to talk about in this post. Invoke is fun and all, but I wanted to show how I’ve integrated it with another more robust tool I’ve written, Gatekeeper. The goal of Gatekeeper is to make a simple drop-in authentication system for applications to take care of a lot of the boilerplate user management needs. It comes with the usual CRUD handling for users, groups and permissions (RBAC) and also supports password resets, security questions and “remember me” functionality. Again, Gatekeeper is a cool library but it’s not the primary focus here. I wanted to integrate the two libraries so I could let each do what they do best – Invoke to check the current user against a set of criteria and Gatekeeper to provide the data for this validation.
Invoke lets you hook in your own users via a `UserInterface` that you can implement in your own application. In this case Gatekeeper has a concept of users too, but they don’t exactly mesh with what Invoke is expecting. So, let’s make an Invoke-compatible user object that it can use for it’s checks. This is the real key to the integration:
<?php use \Psecio\Gatekeeper\Gatekeeper as Gatekeeper; class InvokeUser implements \Psecio\Invoke\UserInterface { private $details = array(); public function __construct(array $details) { $this->details = $details; } public function getGroups() { $groupSet = array(); $groups = Gatekeeper::findUserById($this->details['id'])->groups; foreach ($groups as $group) { $groupSet[] = new InvokeGroup($group); } return $groupSet; } public function getPermissions() { $permSet = array(); $permissions = Gatekeeper::findUserById($this->details['id'])->permissions; foreach ($permissions as $permission) { $permSet[] = new InvokePermission($permission); } return $permSet; } } ?>
Then, we’ll define the Invoke configuration in a YAML document:
event/add: protected: on groups: [test] permissions: [perm1]
In this case we’re telling Invoke that when it sees the requested URL of `/event/add` it should check a few things:
- That the user is authenticated (protected: on)
- That the user has a group with the “name” attribute of “test”
- That the user has a permissions with the “name” attribute of “perm1”
If the user passes all of these checks, they’re good to go. Here’s how that would look in the execution of the Invoke code:
<?php $en = new \Psecio\Invoke\Enforcer(__DIR__.'/config/routes.yml'); // If you're already using Gatekeeper for user management, you // can just use this: $userData = Gatekeeper::findUserById(1)->toArray(); // Otherwise you can push in your own user data $userData = array( 'username' => 'ccornutt', 'id' => 1, 'email' => 'ccornutt@phpdeveloper.org' ); $allowed = $en->isAuthorized( new Confer\InvokeUser($userData), new \Psecio\Invoke\Resource() ); if ($allowed === false) { // They're not allowed on this resource, forward to an error! } ?>
The Invoke Resource
by default looks at the current REQUEST_URI
value so no options are needed when it’s created.
I’ve found this a pretty simple way to integrate these two libraries while still maintaining the correct separation of concerns enough to let each tool do their job. I’m always welcome to feedback on both projects or, of course, PRs if you find something that needs improving or a bug to fix.
Here’s more information about each of them:
- Invoke on GitHub
- Gatekeeper on GitHub
- Gatekeeper full documentation
One comment