When to use Gate::after in Laravel
In a Laravel app policies are a great way to organize authorization logic that revolves around models.
For the longest time, I've been using Gate::before
to allow superadmins to do anything they want. While working on a new app, it finally clicked how Gate::after
can be useful too. I'd like to share that knowledge in this blog post.
This post assumes that you understand and have experience with Laravel's authorization features. If not, read the relevant documentation first.
Explaining Gate::before
The new app our team is building that was mentioned in the intro, is called attended.io. When finished, it will allow event organizers to register their events, slots, and speakers. Attendees can leave feedback on the slots of the events and its speakers.
Let's take a look at a part of the policy that handles whether or not a user is allowed to administer an event.
class EventPolicy
{
use HandlesAuthorization;
public function administer(User $user, Event $event)
{
return $user->organizes($event);
}
}
With this policy you can make the auth check like this:
$user->can('administer', $event); // returns a boolean
This will return true
for users that organize the event, and false
for all others.
In our system, we also have superadmins that are allowed to do anything. They do not organize the event, but they still should be allowed to administer it. This can be solved by adding a Gate::before
:
// somewhere in a service provider
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
});
The closure passed to Gate::before
will be used before the EventPolicy
(and all other policies) get called. If you're a superadmin you can now administer the event even though you're not organizing that event. Life is good!
Notice that if you're not a superadmin we don't return false
but null
(by not returning anything). This is done on purpose. If we were to return false
here, the policies classes wouldn't be checked. Only superadmins would be able to administer events.
Explaining Gate::after
In our attended.io app, a slot can be reviewed by users that are logged in. A slot can only be reviewed once per user. A user may not review a slot before the slot has started.
Here's the policy class:
class SlotPolicy
{
use HandlesAuthorization;
public function review(User $user, Slot $slot)
{
if ($user->hasReviewed($slot)) {
return false;
}
if ($slot->starts_at->isFuture()) {
return false;
}
return true;
}
}
For non-superadmins this works fine. All our rules are respected. But the Gate::before
- mentioned in the previous section - grants our superadmins way too much power. Superadmins can now review slots multiple times, and they can review a slot before it starts. Let's fix that!
Instead of using Gate::before
, let's use Gate::after
.
// somewhere in a service provider
Gate::after(function ($user, $ability) {
return $user->isSuperAdmin();
});
With this Gate::after
in place instead of Gate::before
the policies will get called first, even for superadmins. All users, including superadmins, are now unable to review slots that haven't started yet. Great!
If you have a keen eye, you probably noticed that for the review
check on SlotPolicy
the Gate::after
doesn't get called because SlotPolicy
already returns true
. And that's true, for that particular check will work without the Gate::after
.
But let's take a look again at the EventPolicy
from the previous section.
class EventPolicy
{
use HandlesAuthorization;
public function administer(User $user, Event $event)
{
return $user->organizes($event);
}
}
As it stands, superadmins are not allowed to administer events because administer
returns a boolean. Let's fix that.
class EventPolicy
{
use HandlesAuthorization;
public function administer(User $user, Event $event)
{
if ($user->organizes($event)) {
return true;
}
}
}
Now we don't return anything for regular users that don't organize the event. In this case our Gate::after
callback will get called and it will return true
for superadmins, false
for all other users.
The only issue I seem to run into with this is when using blade directives like
@can('withdraw', $post)
to render buttons when items are at different lifecycle stages. This causes all of the action buttons to be visible to admin users, whether the action should be avaiable or not. Any suggestions on a better way to do that?