Releasing Bouncer: Roles and Permissions for Laravel apps
After many years in development, 56 releases, 1.3 million downloads, and over 2,800 unsolicited stars, Bouncer has finally reached version 1.0. It has been extremely reliable and stable for quite a while now, and is being used in production by countless apps worldwide.
This is a personal update, with some musings about my journey through the years - from inception to final release. For technical information on how to use Bouncer day to day, check out the extensive documentation or listen to me discuss it with Matt Stauffer on The Laravel Podcast.
As soon as I started playing with this, I knew it will be the future of ACL for all Laravel apps. It's just so good. Taylor has this amazing sense for clear and intuitive APIs, and the Gate abstraction really brought that to light.
However, one thing was missing from the built-in authorization system: dynamic permissions, stored in the database. The way the gate is built, all checks are performed by hard-coded functions defined in your app, so there's no way to allow your admins to control any of it at runtime via some dashboard UI. As Taylor's original commit clearly states:
[The built-in gate] gives a structure to organizing logic that authorizes actions on entities. It does not make any determination on how "user roles" are
stored
At the time, there were many other popular ACL systems which did support tweaking permissions at runtime, but they had one major drawback: they were all built prior to Laravel's Gate. They were completely separate systems; if you decided to use them, you would forgo all of the niceties and beautiful integrations that Laravel's gate offered.
So I decided to build an open source package that would get you the best of both worlds: dynamic DB-driven permissions, fully integrated with Laravel's gate. We made some improvements to the gate checks in Laravel 5.3, making it more streamlined and predictable, thus easier to store those abilities in the DB.
The name Bouncer came to me pretty early on. A bouncer's duty is to provide security at the gate and to check people's permissions. So this was a very natural paring to the Gate in Laravel.
Interestingly, the logo designer I was working with at the time (who is not a native English speaker) did not get the reference. Here are some original logos he designed:
The two to the right are clearly inspired by a bouncing motion. Oof! Hopefully native speakers do get it right away.
After quickly clarifying the meaning of the word bouncer, we started iterating on actual bouncers. We tried friendly bouncers, threatening bouncers, bearded bouncers, square-jawed bouncers, and a ton of different variations. Here are just a few:
I'm quite fond of what we ultimately ended up with:
It exudes a strong sense of security, but the roundness makes it feel friendlier and less threatening.
Bouncer's raison d'etre is to seamlessly integrate with Laravel's gate. To achieve that, I set out with a single goal in mind: you should only have to interact with Bouncer when assigning roles and abilities to users. For the actual authorization checks, Laravel's own hooks throughout the system should work automatically, without any special Bouncer syntax.
The way Bouncer hooks into Laravel's gate checks is pretty simple. The Gate lets you define a global before callback, which gets called before any other checks you've defined: if your before callback allows or disallows an action, no further checks will run.
While the before callback was originally intended for stuff like "allow everything for admins", I immediately realized that this would be the perfect place to hook up dynamic checks, allowing me to query the database for any permissions. This is how it originally worked (we later switched it to using the after callback - you can read more about that in this thread).
From the get-go, documentation has been extremely important to me. Open source projects live or die by their documentation, so I wanted Bouncer's docs to be the best it possibly can. And especially in the Laravel ecosystem, Taylor has set an extremely high bar for meticulous documentation.
In a way, clear docs are sometimes even more important than the code itself. Without telling your users how to use your tool, very few of them will source dive to figure it out. They'll just move on to the next thing.
I attribute a lot of Bouncer's success to the clear documentation, but there's still a lot more to do on that front. Being the creator and having a clear picture of the whole puzzle, it's easy to forget what people new to the tool struggle with.
For example: as stated earlier, Bouncer is only meant to be used for assigning roles and permissions to users. The actual authorization checks are to be handled like you would in any standard Laravel app. So I figured I don't have to repeat all that, since it's clearly outlined in the Laravel docs. Nevertheless, I still see people struggle with that a lot. They set up their their roles and permissions, then don't know where to go from their. That's an area I still want to flesh out in the docs.
Let me just state this right now: postponing the 1.0 release till now has done my users a disservice. Bouncer has been stable for years, and actively used in production the world over. And yet, I was always hesitant to release it, since I knew there's so much more I want to add to it. I discussed this at length with Matt on the podcast: I fell down the trap of wanting to make it perfect before releasing it, which is obviously impossible. As Voltaire has warned: "Perfect is the enemy of good".
So as I'm releasing version 1.0 of Bouncer, there are still 2 outstanding features that I really wanted to have included in the initial release, but didn't make the cut:
Per-model roles. For the longest time, people have been clamoring to have a way of assigning roles to users only for given model (or model class). Here's what that code would look like:
// Note: this is not implemented yet
Bouncer::allow('editor')->to(['view', 'edit'])->everything();
Bouncer::assign('editor')->to($user)->for(Invoice:class);
With that, the user would be able to do see and edit all invoices, but nothing else. This can of course now be done directly without a role, but doing it through a role would provide another level of flexibility.
I've tried tackling this many times, but it turns out to be quite tricky, because caching becomes a real nightmare. I still hope to tackle it one day. We'll see.
Ability constraints. Allowing arbitrary constraints on a given ability would add way more granular control:
// Note: this is not implemented yet
Bouncer::allow($user)
->to('view', Post::class)
->where('is_confidential', false);
If you go source diving in Bouncer, you'll find some code and tests where I started implementing this. It's far from done, but stay tuned.
Overall, Bouncer is in a really great place. Every good product has a long roadmap, and thinking I can get to the end of that road before releasing a 1.0 was both foolish and unrealistic.
Well that's a wrap. I hope you try Bouncer in your app, and enjoy using it. Bouncer's API is designed to feel like prose, with every method call reading like a proper English sentence. Try it, and let me know if you get that feeling too!
Questions? Comments? Complaints?
Ping me on Twitter.
Let's take a look at a neat little feature coming in PHP 7.1: easily converting any callable into a proper Closure using the new Closure::fromCallable() method.