简体   繁体   中英

Checking FormRequest's validation rules before authorization in Laravel

I'm implementing an API in Laravel using JSON:API specification.

In it I have a resource, let's call it Ponds, with many-to-many relationships with another resource, let's call it Ducks.

According to JSON:API specs in order to remove such relationship i should use DELETE /ponds/{id}/relationships/ducks endpoint, with request of following body:

{
    "data": [
        { "type": "ducks", "id": "123" },
        { "type": "ducks", "id": "987" }
    ]
}

This is handled by PondRemoveDucksRequest, which looks as follows:

<?php
...
class PondRemoveDucksRequest extends FormRequest
{
    public function authorize() 
    {
        return $this->allDucksAreRemovableByUser();
    }

    public function rules()
    {
        return [
            "data.*.type" => "required|in:ducks",
            "data.*.id" => "required|string|min:1"
        ];
    }

    protected function allDucksAreRemovableByUser(): bool
    {
        // Here goes the somewhat complex logic determining if the user is authorized 
        // to remove each and every relationship passed in the data array.
    }
}

The problem is that if I send a body such as:

{
    "data": [
        { "type": "ducks", "id": "123" },
        { "type": "ducks" }
    ]
}

, I get a 500, because the authorization check is triggered first and it relies on ids being present in each item of the array. Ideally I'd like to get a 422 error with a standard message from the rules validation.

Quick fix I see is to add the id presence check in the allDucksAreRemovableByUser() method, but this seems somewhat hacky.

Is there any better way to have the validation rules checked first, and only then proceed to authorization part?

Thanks in advance!

1 - Create abstract class called "FormRequest" inside App\\Requests directory and override the validateResolved() method:


<?php

namespace App\Http\Requests;

use Illuminate\Validation\ValidationException;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;

abstract class FormRequest extends BaseFormRequest
{

    /**
     * Validate the class instance.
     *
     * @return void
     * @throws AuthorizationException
     * @throws ValidationException
     */
    public function validateResolved()
    {

        $validator = $this->getValidatorInstance();

        if ($validator->fails())
        {
            $this->failedValidation($validator);
        }

        if (!$this->passesAuthorization())
        {
            $this->failedAuthorization();
        }
    }

}

2 - Extend your FormRequests with custom FormRequest

<?php

namespace App\Http\Requests\Orders;

use App\Http\Requests\FormRequest;

class StoreOrderRequest extends FormRequest
{



}


add $this->getValidatorInstance()->validate(); at beggining of authorize() method

Here is a slightly different approach than what you are attempting, but it may accomplish the desired outcome for you.

If you are trying to validate whether the given duck id belongs to the user, this can be done in the rule itself as follows:

"data.*.id" => "exists:ducks,id,user_id,".Auth::user()->id

This rule asks if a record exists in the ducks table which matches the id and where the user_id is the current logged in user_id.

If you chain it to your existing rules (required|string|min:1), using 'bail', then it wouldn't run the query unless it had passed the other three rules first:

"data.*.id" => "bail|required|string|min:1|exists:ducks,id,user_id,".Auth::user()->id

The most cleanest solution I found to solve it was by creating a small trait for the FormRequest and use it anytime you want to run validation before the authorization, Check the example bellow:

<?php

namespace App\Http\Requests\Traits;

/**
 *  This trait to run the authorize after a valid validation
 */
trait AuthorizesAfterValidation
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     *  Set the logic after the validation
     * 
     * @param $validator
     * @return void
     */
    public function withValidator($validator)
    {
        $validator->after(function ($validator) {
            if (! $validator->failed() && ! $this->authorizeValidated()) {
                $this->failedAuthorization();
            }
        });
    }

    /**
     *  Define the abstract method to run the logic.
     * 
     * @return void
     */
    abstract public function authorizeValidated();
}

Then in your request class:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use App\Http\Requests\Traits\AuthorizesAfterValidation;

class SomeKindOfRequest extends FormRequest
{
    use AuthorizesAfterValidation;

    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorizeValidated()
    {
        return true; // <---- Set your authorization logic here
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
        ];
    }
}

Source https://github.com/laravel/framework/issues/27808#issuecomment-470394076

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM