Laravel Policies for nested shallow ressources
Asked Answered
O

2

6

I have a the following routes defined:

//Leases
Route::resource('properties.leases', LeaseController::class)
        ->only(['show'])
        ->shallow();

//Invoices
Route::resource('leases.invoices', InvoiceController::class)
        ->only(['index', 'show'])
        ->shallow();

The above generates the following urls:

| GET|HEAD  | leases/{lease}                                             | App\Http\Controllers\LeaseController@show                                       |
| GET|HEAD  | leases/{lease}/invoices                                    | App\Http\Controllers\InvoiceController@index                                    |
| GET|HEAD  | invoices/{invoice}                                         | App\Http\Controllers\InvoiceController@show                                     |

The relationships are as below:

Properties hasMany Leases.
Leases hasMany Invoices.

I am trying to authorize these routes, so only users who:

  1. Belongs to the same team that the "Leases" and "Invoices" also belong to.
  2. Is currently logged in on that team.

In my AuthServiceProvider I have defined the following policies:

protected $policies = [
    Lease::class => LeasePolicy::class,
    Invoice::class => InvoicePolicy::class,
];

In my LeaseController I have defined the authorization check:

public function __construct()
{
    $this->authorizeResource(Lease::class, 'lease');
}

The LeasePolicy looks like this:

public function view(User $user, Lease $lease)
{
    //Does the current user belong to the team that the lease is associated with
    //and is the user's current team the same one?
    $team = $lease->property->team;
    return $user->belongsToTeam($team) && $user->isCurrentTeam($team);
}

And in my InvoiceController I have defined this:

public function __construct()
{
    $this->authorizeResource(Invoice::class, 'invoice');
}

The InvoicePolicy looks like this:

/**
 * Path: leases/{lease}/{$invoice}
 */
public function viewAny(User $user)
{

    //When users go to this path I can only access $user here. 
    //How to check if the user can even access the $lease.

}

/**
 * Path: invoices/{$invoice}
 */
public function view(User $user, Invoice $invoice)
{
    //Does the current user belong to the team that the lease is associated with
    //and is the user's current team the same one?
    $team = $invoice->lease->property->team;
    return $user->belongsToTeam($team) && $user->isCurrentTeam($team);
}

In my application, I have a lot of routes that are "under" the /lease/{lease}/{model} route, e.g.:

//Files
Route::resource('leases.files', FileController::class)
        ->only(['index'])
        ->shallow();

For these, how can I define my Policies so only users who are allowed to view these ressources can get access?

Otoole answered 8/2, 2022 at 7:32 Comment(0)
L
3

As far as I know at the time I wrote this answer, the authorizeResource cannot be used on some shallow nested methods (such as the index, create, & store). So instead, you can call authorize function on each method via controller helpers.

Or if you still want to use authorizeResource, you can only call authorize manually on some shallow nested methods like the following example:

class InvoiceController extends Controller
{
    public function __construct()
    {
        $this->authorizeResource(Invoice::class, 'invoice');
    }

    /**
     * Get the map of resource methods to ability names.
     *
     * @return array
     */
    protected function resourceAbilityMap()
    {
        return collect(parent::resourceAbilityMap())
            ->except(['index', 'create', 'store'])
            ->all();
    }

    /**
     * Display a listing of the resource.
     *
     * @param \App\Models\Lease $lease
     * @return \Illuminate\Http\Response
     */
    public function index(Lease $lease): Response
    {
        $this->authorize('view', $lease);

        ...
    }
}

And by the way, if you want to make InvoicePolicy simpler, you can also reuse LeasePolicy like the following code:

class InvoicePolicy
{
    use HandlesAuthorization;

    public function __construct(
        protected LeasePolicy $leasePolicy,
    ) {
    }

    /**
     * Determine whether the user can view the model.
     *
     * @param  \App\Models\User  $user
     * @param  \App\Models\Invoice  $invoice
     * @return \Illuminate\Auth\Access\Response|bool
     */
    public function view(User $user, Invoice $invoice): bool
    {
        return $this->leasePolicy->view($user, $invoice->lease);
    }
}
Lowering answered 15/2, 2022 at 3:35 Comment(0)
C
1

Manually registering policies is not needed anymore, InvoicePolicy will be registred as an Invoice policy (see Policy Discovery).

namespace App\Policies;

use App\Models\Invoice;
use App\Models\Lease;
use App\Models\User;

class InvoicePolicy
{
    public function viewAny(User $user, Lease $lease): bool
    {
        //
    }

    public function view(User $user, Invoice $invoice, Lease $lease): bool
    {
        //
    }
}

The first element of the array passed to the authorize method determines which model will be used. It can either be an instance or the class name (see Methods without Models and Actions That Don't Require Models).

Gate facade replaces the controller helper authorize suggested by @yusuf-t.

namespace App\Http\Controllers;

use App\Models\Invoice;
use App\Models\Lease;
use App\Models\User;

use Illuminate\Support\Facades\Gate;

class InvoiceController extends Controller
{
    // leases/{lease}/invoices
    public function index(Lease $lease)
    {
        Gate::authorize('viewAny', [ Invoice::class, $lease ]);

        //
    }

    // leases/{lease}/invoices/{invoice}
    public function show(Lease $lease, Invoice $invoice)
    {
        Gate::authorize('view', [ $invoice, $lease ]);

        //
    }
}
Carissacarita answered 27/10 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.