Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define policy for a list or array in laravel?

I have the following policy which determines if a user is able to view a contract.

public function view(User $user, Contract $contract)
    {
        if ($user->user_type->id == 2) { // If user is a vecino
            if ($user->id == $contract->customer_id) {
                return true;
            }
        } else if ($user->user_type->is_admin == true) { // If user is an admin
            return true;
        }

        return false;
    }

Which is then checked for authorization with

$this->authorize('view', $contract);

How do I check authorization for a list/array/collection? Like if I get a list of contracts via Contract::all()

I haven't found any way to do this. I could do a loop and call $this->authorize for every iteration to check for authorization but that might impact performance.

Is there a better way of doing this?

like image 626
Rick Avatar asked May 16 '19 10:05

Rick


2 Answers

One solution I am currently using is a hybrid approach where you define your rules within a scope and then reference that scope from the policy allowing you to reuse your authorization logic.

// Contract model

public function scopeViewable($query)
{
    // If the user is admin, just return the query unfiltered.
    if (Auth::user()->user_type->is_admin) {
        return $query;
    }

    // Check the contract belongs to the logged in user. 
    return $query->where('customer_id', Auth::id());
}

And then in your policy, reference that scope but restrict it to the current model. Make sure to return a boolean using exists(). This essentially checks that your model is viewable.

// Contract Policy

public function view(User $user, Contract $contract)
{
    return Contract::viewable()
        ->where('id', $contract->id)
        ->exists()
    ;
}

Importantly, you should use the scope when retrieving a collection of models and not the policy which would run the scope query for each model in the collection. Policies should be used on individual model instances.

Contract::viewable()->paginate(10);

// Or
Contract::viewable()->get();

But, when you want to check an individual contract you can use your policy directly.

$this->authorize('view', $contract);

// Or
Auth::user()->can('view', [Contract::class, $contract]);
like image 139
waterloomatt Avatar answered Sep 17 '22 23:09

waterloomatt


The design i often sees in this case, is to check if all elements in the query is allowed to be viewed through the policy. This does not scale well and works bad with pagination.

Instead of filtering out the contracts with policies, the better solution is to filter the contracts already in the query. This mainly because if you want to do pagination down the line, you want to do all filtering before the query is executed to avoid having weird pagination meta data. While also having to run n operations for each element, which would already be a problem at 1000 elements.

There for doing the following query clause, can obtain the same result as your policy.

Contract::where('user_id', $user->id)->get();

A version of this i usually do to make things easier for my self is creating a scope in the user model.

public function scopeOwned($query, User $user)
{
    return $this->query->where('user_id', $user->id);
}

Contract::owned($user)->get();
like image 21
mrhn Avatar answered Sep 18 '22 23:09

mrhn