8

In my Laravel 5.8 project I am implementing a reputation system similar to Stack Exchange's one: for example, users can reply to a discussion only if they have "Level 3" reputation.

I wanted to use Laravel's policies system to build the permissions logic like so in my DiscussionPolicy file:

public function reply(User $user)
{
    $result = true;
    if ($user->current_level < 3) {
        $result = false;
        //I want to inject a custom error message here
    }
    return $result;
}

Everything works, but users get a 403 page without any explanation, and I wanted to find an elegant way to tell them that they cannot perform that action because they don't have Level 3.

Can you please suggest a way to inject somehow this message, to show it in my custom 403.blade.php page? I've been able to do this by flashing a variable in the session, but I don't think it's elegant, I would like to use something like a MessageBag (Illuminate\Support\MessageBag).

LARAVEL 8.x : check this answer.

Marco Cazzaro
  • 661
  • 8
  • 13
  • I'd probably throw a `Illuminate\Auth\Access\AuthorizationException` with a helpful message and let the exception handler show the correct view with the message. Using the MessageBag seems quite convoluted for relatively straightforward functionality to me. Bonus is that you can setup the handler very generically ( return view with message that's in the exception ) and your policy code becomes quite readable since you only need to throw an exception with a message. – Loek Jun 06 '19 at 08:31
  • Thanks @Loek! Since I am using policies, all the exceptions are fired automatically, what I am missing is how to interact with the exception that is automatically fired. If I have to handle the exception myself I am doing something that is not straightforward either. Maybe Laravel is missing something here? – Marco Cazzaro Jun 06 '19 at 08:35
  • 2
    Hmm, that's true. Knowing Laravel, it's probably possible but it's not in the documentation. A quick search leads me to the `deny()` function instead of the `return false`, maybe that's better? https://stackoverflow.com/a/55717806/4074200 and https://laravel.com/api/5.8/Illuminate/Auth/Access/HandlesAuthorization.html – Loek Jun 06 '19 at 08:40
  • 1
    Awesome! This is exactly what I was looking for. Thanks! If you post it as a reply, I'll mark it as a solution of my problem. – Marco Cazzaro Jun 06 '19 at 08:46

3 Answers3

10

Answer was given in comments, put here for reference:

Laravel provides this functionality through the deny() function in the HandlesAuthorization trait. The deny() function throws an UnauthorizedException but allows you to specify a message instead of throwing a plain exception.

Replace the return false with it and you can send custom messages to render in the exception handler.

Example:

public function reply(User $user)
{
    if ($user->current_level < 3) {
        $this->deny('Sorry, your level is not high enough to do that!');
        // Laravel 6+ requires you to return the deny(), see following line
        // return $this->deny('Sorry, your level is not high enough to do that!');
    }
    return true;
}
Loek
  • 4,037
  • 19
  • 35
  • 6
    You have to `return $this->deny()`, at least on Laravel 6 – dsturbid Mar 17 '20 at 10:01
  • I've tried both `return $this->deny()` and `$this->deny` in my Laravel 7 application, but the custom message never gets returned in the response. Any advice? – JasonJensenDev Aug 24 '20 at 19:09
  • According to the updated docs, nothing has changed since L6. Returning the `deny` should do the trick https://laravel.com/api/7.x/Illuminate/Auth/Access/HandlesAuthorization.html . Are you sure that you actually get the exception you expect? – Loek Aug 25 '20 at 07:28
  • 1
    @BakerStreetSystems because the error view is fixed to `__('Forbidden')` instead change it manually to `@section('message', $exception->getMessage() ?: __('Forbidden'))` – ctf0 Jan 28 '21 at 18:08
7

I thought I'd add this answer as I arrived here searching for how to return messages from policy methods as the code I'm maintaining only returns false when an action is not allowed.

The current documentation says to return an Illuminate\Auth\Access\Response instance from your policy method:

...
use Illuminate\Auth\Access\Response;

public function update(User $user, Post $post)
{
    return $user->id === $post->user_id
                ? Response::allow()
                : Response::deny('You do not own this post.');
}
Tony
  • 9,672
  • 3
  • 47
  • 75
  • ``$this->authorize('create', new Reply)`` always return 403 status code even though I've just set a custom one in ReplyPolicy ```Response::deny('You are posting too much', 429)```. Is there any clean solution for this? – AmirRezaM75 Mar 27 '21 at 17:19
  • 1
    @AmirRezaM75 - It's probably best to post a new question so you can include the relevant code and give more of a description. If you comment here again when you have I can take a look. – Tony Mar 28 '21 at 18:31
0

Here you go:

class DiscussionPolicy
{
    use HandlesAuthorization;

    public function reply(?User $user)
    {
        if(!$user) return $this->deny("Your access denied.");
    }
}
X 47 48 - IR
  • 1,250
  • 1
  • 15
  • 28