d3jn
d3jn

Reputation: 1410

How can we return reason for action denial from inside Laravel policy?

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

Answers (3)

Michael Mitch
Michael Mitch

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 boolean
  • Gate::inspect() will give full response

Given 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

d3jn
d3jn

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

Mihir Bhende
Mihir Bhende

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

Related Questions