Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

lemonidas's avatar

Policy works for 'index','create','store' but not for 'view','edit','delete'

Hello, I'm trying to add policies to my project and I'm getting this weird issue.

I had the Controller and Route::resource working with auth just fine. I created the Policy with artisan. Then I added a before function to allow admins to access everything. Finally I added

public function __construct()
    {
        $this->middleware('auth');
        $this->authorizeResource(Fee::class, 'fees'); 
        // dd($this->getMiddleware());
    }

to my controller's __construct method and the policy binding in AuthServiceProvider

 protected $policies = [
        // 'App\Models\Model' => 'App\Policies\ModelPolicy',
         'App\Models\Fee' => 'App\Policies\FeePolicy',
    ];

The Policy file is simple

<?php

namespace App\Policies;

use App\Models\Fee;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Log;



class FeePolicy
{

    /**
     * The before method of a policy class will not be called if the 
     * class doesn't contain a method with a name matching the name of the ability being checked.
     */


    use HandlesAuthorization;
    
    protected $authorizedRoles = [
        'platform-manager',
    ];

     /**
     * Perform pre-authorization checks.
     *
     * @param  \App\Models\User  $user
     * @param  string  $ability
     * @return void|bool
     */
    public function before(User $user)
    {
        // return null;
        //when viewing the index route this works as expected
        //when trying to view a specific item /fees/1 or /fees/1/edit or delete it never gets here
        Log::info('checking role BEFORE entering specific policy actions');
        if ($user->hasAnyRole($this->authorizedRoles)) {
            return true;
        }
    }
    /**
     * Determine whether the user can view any models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function viewAny(User $user)
    {
        //if I return null in before() we get in this one
        Log:info('Running fee policy INDEX');
        return true;
    }

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Fee  $fee
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function view(User $user, Fee $fee)
    {
        //it never gets to log this
        Log:info('Running fee policy for VIEW');
        return true;
    }

    /**
     * Determine whether the user can create models.
     *
     * @param  \App\Models\User  $user
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function create(User $user)
    {
        return true;
    }

    /**
     * Determine whether the user can update the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Fee  $fee
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function update(User $user, Fee $fee)
    {
        return true;
    }

    /**
     * Determine whether the user can delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Fee  $fee
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function delete(User $user, Fee $fee)
    {
        return true;
    }

    /**
     * Determine whether the user can restore the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Fee  $fee
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function restore(User $user, Fee $fee)
    {
        return true;
    }

    /**
     * Determine whether the user can permanently delete the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Fee  $fee
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function forceDelete(User $user, Fee $fee)
    {
        return true;
    }
}

This is the output when I do dd($this->getMiddleware() in the Controller's __construct.

array:6 [▼
  0 => array:2 [▼
    "middleware" => "auth"
    "options" => []
  ]
  1 => array:2 [▼
    "middleware" => "can:viewAny,App\Models\Fee"
    "options" => array:1 [▼
      "only" => array:1 [▼
        0 => "index"
      ]
    ]
  ]
  2 => array:2 [▼
    "middleware" => "can:view,fees"
    "options" => array:1 [▼
      "only" => array:1 [▶]
    ]
  ]
  3 => array:2 [▼
    "middleware" => "can:create,App\Models\Fee"
    "options" => array:1 [▼
      "only" => array:2 [▼
        0 => "create"
        1 => "store"
      ]
    ]
  ]
  4 => array:2 [▼
    "middleware" => "can:update,fees"
    "options" => array:1 [▼
      "only" => array:2 [▶]
    ]
  ]
  5 => array:2 [▼
    "middleware" => "can:delete,fees"
    "options" => array:1 [▼
      "only" => array:1 [▼
        0 => "destroy"
      ]
    ]
  ]
]

Also the output of php artisan routes:list --json is:

[
  {
    "domain": null,
    "method": "GET|HEAD",
    "uri": "fees",
    "name": "fees.index",
    "action": "App\Http\Controllers\FeeController@index",
    "middleware": [
      "web",
      "auth",
      "can:viewAny,App\Models\Fee"
    ]
  },
  {
    "domain": null,
    "method": "POST",
    "uri": "fees",
    "name": "fees.store",
    "action": "App\Http\Controllers\FeeController@store",
    "middleware": [
      "web",
      "auth",
      "can:create,App\Models\Fee"
    ]
  },
  {
    "domain": null,
    "method": "GET|HEAD",
    "uri": "fees/create",
    "name": "fees.create",
    "action": "App\Http\Controllers\FeeController@create",
    "middleware": [
      "web",
      "auth",
      "can:create,App\Models\Fee"
    ]
  },
  {
    "domain": null,
    "method": "GET|HEAD",
    "uri": "fees/{fee}",
    "name": "fees.show",
    "action": "App\Http\Controllers\FeeController@show",
    "middleware": [
      "web",
      "auth",
      "can:view,fees"
    ]
  },
  {
    "domain": null,
    "method": "PUT|PATCH",
    "uri": "fees/{fee}",
    "name": "fees.update",
    "action": "App\Http\Controllers\FeeController@update",
    "middleware": [
      "web",
      "auth",
      "can:update,fees"
    ]
  },
  {
    "domain": null,
    "method": "DELETE",
    "uri": "fees/{fee}",
    "name": "fees.destroy",
    "action": "App\Http\Controllers\FeeController@destroy",
    "middleware": [
      "web",
      "auth",
      "can:delete,fees"
    ]
  },
  {
    "domain": null,
    "method": "GET|HEAD",
    "uri": "fees/{fee}/edit",
    "name": "fees.edit",
    "action": "App\Http\Controllers\FeeController@edit",
    "middleware": [
      "web",
      "auth",
      "can:update,fees"
    ]
  }
]

Which seems to me that the middleware is registered correctly so it should at least get inside the Policy function and I would be able to see the Log. But not the case.

I can't find anything related to this, so any ideas on where I've dropped the ball? What stumbles me is that for the index/create/store route the whole flow works fine, but for the rest of the routes it just doesn't and the User is passed correctly to the Policy since the before() works. On view or edit or delete I get 403 unauthorized.

0 likes
6 replies
lemonidas's avatar

If I authorize the specific action (e.g. view) from within the view function

$fee = Fee::find($id);
$this->authorize('view', $fee);

and remove the blanket resource authorization from the constructor, then it works as intended. But for a bog standard controller action set this seems weird to not work (unless I'm understanding laravel's documentation examples wrong).

lemonidas's avatar

@SilenceBringer thank you for the reply. I changed the show and edit methods to Fee $fee instead of $id and without using the policy they both work, so I guess this means that model binding is working correctly. But still when I use the

public function __construct()
    {
        $this->middleware('auth');
        $this->authorizeResource(Fee::class, 'fees'); 
    }

I still get 403 This action is unauthorized.

SilenceBringer's avatar
Level 55

@lemonidas if you use Route::resource in routes - it must be

        $this->authorizeResource(Fee::class, 'fee'); 
lemonidas's avatar

@SilenceBringer thank you so much! It finally worked. I had tried with fee instead of fees but because my binding was broken it didn't work. I mistakenly thought that it required the url string in the authorize function.

Now it seems to be working correctly for all actions. Thank you.

Please or to participate in this conversation.