Reputation: 1410
Let's say we have action in the policy for our model that can return false
in bunch of different scenarios:
class PostPolicy
{
public function publish(User $user, Post $post)
{
if ($post->user_id !== $user->id) {
return false;
}
return $post->show_at->lessThan(now());
}
}
As you can see, we are denying this user his publishing rights in two cases: if it's not his post or if this post was prepared in advance for some future date that is not yet due.
How can I provide some context as to why authorization failed? Was it because we are not the owner or was it because it's not time yet for this post to be published?
$user->can('publish', $post); // if this returns false we don't know
// what caused authorization to fail.
It looks that Laravel policies by design doesn't have any way of doing that. But I am curious as to what workarounds there can possibly be so that we can have authorization logic (no matter how intricate) in one place (model's policy) and also get some context (i.e., custom error codes) when authorization fails.
Any ideas?
Upvotes: 1
Views: 1890
Reputation: 839
In case any one needed, apart from above accepted answer, in Laravel 7+, Gate can provide reasons for denial,
Reference: https://laravel.com/docs/7.x/authorization#gate-responses
Gate::authorize()
calls will throw reason along with the exception with message provided in the Response::deny(<message>)
call, the exception itself will be an Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
$user->can()
or Gate::allows()
will give booleanGate::inspect()
will give full responseGiven that you will now return a Gate Response object instead of boolean, Laravel will help on returning suitable response stated above.
<?php
use Illuminate\Auth\Access\Response;
Originally, you only return a boolean
<?php
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id !== $user->id;
}
}
By using Gate Response, you can now provide a reason
<?php
use Illuminate\Auth\Access\Response;
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id === $user->id
? Response::allow()
: Response::deny('You are not the author of the post.');
}
}
Upvotes: 6
Reputation: 1410
What I ended up doing was splitting some of the responsibilities between model and it's policy.
Policy ended up responsible for making sure user has the right to do a specific action. Nothing more or less:
class PostPolicy
{
public function publish(User $user, Post $post)
{
return $post->user_id !== $user->id;
}
}
Model on the other hand must have logic to check whether or not certain action can be performed with it:
class Post extends Model
{
...
public function isPublishable()
{
return $this->show_at->lessThan(now());
}
...
}
Therefore each post instance now can tell us whether or not it can be published. Lastly my Post::publishBy(User $user)
action will include authorizing user for this action first and checking if this post can be published separately so that we can determine specific reason as to why publishing failed.
I feel like this design suits better, leaving Laravel policies to do only what they are supposed to be doing (authorizing user actions) and requiring models to be responsible for things that only concern them.
Upvotes: 0
Reputation: 9055
If in controller you are using the policy like :
<?php
$this->authorize('publish', Post::class)
Then laravel is going to have 403 HTTPResponse error.
What you should do is, one policy method should just be checking one validation case.
For example update your policy :
<?php
// Check if post he is trying to publish is his own
public function publishOwnPost(){...}
// Check if post is for future purpose
public function publicFuturePost(){...}
Then in controller do :
<?php
if(!$user->can('publishOwnPost', $post)){
// Return custom error view for case 1
return response()->view('errors.publishOwnPostError', $data, 403);
}
if(!$user->can('publishFuturePost', $post)){
// Return custom error view for case 2
return response()->view('errors.publishFuturePostError', $data, 403);
}
// Do further processing
Upvotes: 0