Security Tip: Don't Forget About Policy Filters!
[Tip#2] Policy Filters let you implement shared authorisation checks across your entire policy without repeating code in every method.
👉 Looking to dive deeper into Laravel security? Check out Practical Laravel Security, my hands-on security course that uses interactive hacking challenges to teach you about how vulnerabilities work, so you can avoid them in your own code! 🕵️
👉 When was your last Security Audit or Penetration Test? Book in a Laravel Security Audit and Penetration Test today! 🕵️
Laravel includes an authorization feature known as Policies, which allow you to wrap up authorization rules relating to a specific model within a class. You can find all the details in the Laravel documentation, but in a nutshell, you have these main methods within the policy, which reflect the different abilities a user can perform on a model:
viewAny(User $user)
view(User $user, Tip $tip)
create(User $user)
update(User $user, Tip $tip)
delete(User $user, Tip $tip)
restore(User $user, Tip $tip)
forceDelete(User $user, Tip $tip)
As a reference, here is a blank policy object: https://gist.github.com/valorin/f51de37759eeac84eb75433d7bbcd25c.
Policy objects on their own are great, and give you a lot of flexibility when setting up authorisation rules within your apps. However, consider the scenario where you have admin users who are allowed every ability. You will find yourself repeating this multi-conditional pattern in every policy method:
public function view(User $user, Tip $tip)
{
return $user->admin || $tip->user_id === $user->id;
}
While this does work, it’s ugly and relies on you including the admin conditional in every single method1. If that behaviour changes (maybe you switch from an admin flag to user level), you’re stuck updating every single instance of the conditional in a class… which isn’t fun. Or maybe you need to add another check - a moderator has admin powers for this specific model. Now you’ve got three conditionals replicated in every ability method… 😔
The solution is to use a Policy Filter!
A Policy Filter is a before()
method that runs before the ability specific methods run. It allows you to define your policy-wide rules once, removing the code duplication.
public function before(User $user, $ability)
{
if ($user->isAdmin() || $user->isTipModerator()) {
return true;
}
}
The before()
method has three return states:
true
- Grant access to the user, skipping the ability specific method.false
- Block access to the user, skipping the ability specific method.null/void
- Run the ability specific method to check access.
With these states, you can easily use this feature to do things like allowing admin users or blocking suspended users, without making a mess of your ability-specific logic. You could also wrap the before()
method into a trait to reuse between policies.
To see it in action: https://gist.github.com/valorin/c7830b9e300db3ef977c11657659ac7b
Which is easy to forget!