laravel formrequest перед промежуточным программным обеспечением

Я знаю, это сложный случай, но, возможно, кто-то из вас знает, как это сделать.

Концепция

В моем API есть следующий процесс:

  1. Process query string parameters (FormRequest)
    • Replace key aliases by preferred keys
    • Сопоставьте строковые параметры с массивами, если массив не ожидается
    • Установить значения по умолчанию (включая Auth::user() для параметров на основе id)
    • и Т. Д.
  2. Check if the user is allowed to do the request (Middleware)
    • Using processed (validated and sanitized) query params → otherwise I had to do exceptions for every possible alias and mapping as well as checking if the paramter is checked and that doesn't seem reasonable to me.

Проблема

Тем не менее, если вы просто назначаете промежуточное программное обеспечение через ->middleware('middlewareName') маршруту, а FormRequest через внедрение зависимостей в метод контроллера, сначала вызывается промежуточное программное обеспечение, а затем - FormRequest. Как описано выше, мне это не нужно.

Подход к решению

Сначала я попробовал внедрение зависимостей в промежуточном программном обеспечении, но это не сработало.

Мое решение заключалось в назначении промежуточного программного обеспечения в конструкторе контроллера. Здесь работает внедрение зависимостей, но внезапно Auth::user() возвращает null.

Затем я наткнулся на метод FormRequest::createFrom($request) в \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:34 и возможность передать объект $request методу handle() промежуточного программного обеспечения. Результат выглядит так:

public function __construct(Request $request)
{
    $middleware = new MyMiddleware();

    $request = MyRequest::createFrom($request);

    $middleware->handle($request, function() {})
}

Но сейчас запрос еще не подтвержден. Просто вызов $request->validated() ничего не возвращает. Итак, я копнул немного глубже и обнаружил, что $resolved->validateResolved(); выполняется в \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:30, но это, похоже, не запускает проверку, поскольку выдает исключение, в котором говорится, что этот метод не может быть вызван на null, но $ request не null:

Вызов функции-члена validated () при нулевом значении

Теперь я в полном тупике. Кто-нибудь знает, как это решить или я просто не так делаю?

Заранее спасибо!


person shaedrich    schedule 19.03.2021    source источник
comment
Проверка того, разрешено ли пользователю выполнять запрос, не является оптимальным вариантом для промежуточного программного обеспечения, тем более что для политики, которая вызывается в методе authorize () formrequest. Разве это не помогло бы твоей проблеме? и почему для вашего промежуточного программного обеспечения важно, чтобы входные данные были проверены, не могли бы вы просто написать его в целях защиты?   -  person mrhn    schedule 19.03.2021
comment
Что именно вы имеете в виду под защитой? Вот пример, если я использую необработанные входные данные: user_id может быть целым числом или массивом целых чисел, кроме того, user_id также можно передавать как user.id. Было бы неприятно, если бы одна и та же логика выполнялась как в промежуточном программном обеспечении, так и в FormRequest. Итак, я собираюсь изучить политику. Может они решат мою проблему. Спасибо!   -  person shaedrich    schedule 19.03.2021
comment
из-за того, что промежуточное ПО находится на столь ранней стадии жизненного цикла, вы ожидаете, что данные будут плохими, например, если вы хотите проверить там user_id, подтвердите, что это int с помощью intval (), где, как и позже в вашем приложении, вы можете предположить, что это int из-за к запуску проверки.   -  person mrhn    schedule 19.03.2021
comment
Я прочитал документы по политикам, и кажется, что они должны быть привязаны к модели, которая не имеет смысла в случае запроса API, который является более общим, чем выполнение действия с определенной моделью. Можно ли использовать политику без привязки к модели?   -  person shaedrich    schedule 19.03.2021
comment
Но если вы выполняете rest api, в контексте всегда есть модель, но вместо того, чтобы быть общим, в вашем вопросе будьте очень конкретны? и т. д. crud api для команды, которую может обновлять и удалять только администратор   -  person mrhn    schedule 19.03.2021
comment
Это ни REST API, ни CRUD API как таковой. Это конечная точка, которая возвращает агрегированные данные. У агрегированных данных нет таблицы базы данных или представления, с которым могла бы быть связана гипотетическая модель, потому что они агрегируются на лету.   -  person shaedrich    schedule 20.03.2021
comment
Думаю, я обнаружил свои проблемы: пока промежуточное ПО выполняет аутентификацию, я выполнял там авторизацию и поэтому теперь использую Gate. Агрегированные данные поступают из нескольких моделей, поэтому мне придется проверять их все по отдельности. Поскольку это не простое действие CRUD, мне оно кажется немного громоздким. Думаю, Gate - это то, что я собираюсь попробовать, и, вероятно, это сработает лучше всего в моем случае. Спасибо за подсказки!   -  person shaedrich    schedule 22.03.2021
comment
я бы очень хотел увидеть проверки, которые вы выполняете, потому что мне было бы трудно поверить, что это не может быть выполнено в запросе формы   -  person mrhn    schedule 22.03.2021
comment
Это не значит, что это нельзя сделать в FormRequest, но я считаю, что это не предназначено для выполнения FormRequest из-за разделения ответственности. Когда я закончу с изменениями, я отправлю результат в качестве ответа.   -  person shaedrich    schedule 22.03.2021
comment
Но в другой раз очень трудно прийти с предложением, когда код в вопросе не включает в себя то, что вы действительно делаете в качестве проверки. Я бы сказал, что запрос формы будет правильным местом, где промежуточное ПО является более общими терминами.   -  person mrhn    schedule 22.03.2021
comment
Может быть, мой ответ немного лучше это иллюстрирует.   -  person shaedrich    schedule 22.03.2021


Ответы (1)


Думаю, я придумал лучший способ сделать это.

Мое заблуждение

Пока промежуточное ПО выполняет аутентификацию, я выполнял там авторизацию, поэтому мне нужно использовать Gate

Результирующий код

Контроллер

...
public function getData(MyRequest $request)
{
    $filters = $request->query();
    // execute queries
}
...

FormRequest

class MyRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return Gate::allows('get-data', $this);
    }

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

    /**
     * Prepare the data for validation.
     *
     * @return void
     */
    protected function prepareForValidation()
    {
        $this->replace($this->cleanQueryParameters($this->query()));
    }

    private function cleanQueryParameters($queryParams): array
    {
        $queryParams = array_filter($queryParams, function($param) {
            return is_array($param) ? count($param) : strlen($param);
        });
        $defaultStartDate = (new \DateTime())->modify('monday next week');
        $defaultEndDate = (new \DateTime())->modify('friday next week');
        $defaults = [
            'article.created_by_id' => self::getDefaultEmployeeIds(),
            'date_from' => $defaultStartDate->format('Y-m-d'),
            'date_to' => $defaultEndDate->format('Y-m-d')
        ];
        $aliases = [
            // ...
        ];
        $mapper = [
            // ...
        ];
        foreach($aliases as $alias => $key) {
            if (array_key_exists($alias, $queryParams)) {
                $queryParams[$key] = $queryParams[$alias];
                unset($queryParams[$alias]);
            }
        }
        foreach($mapper as $key => $fn) {
            if (array_key_exists($key, $queryParams)) {
                $fn($queryParams, $key);
            }
        }
        $allowedFilters = array_merge(
            Ticket::$allowedApiParameters,
            array_map(function(string $param) {
                return 'article.'.$param;
            }, TicketArticle::$allowedApiParameters)
        );
        $arrayProps = [
            // ..
        ];
        foreach($queryParams as $param => $value) {
            if (!in_array($param, $allowedFilters) && !in_array($param, ['date_from', 'date_to'])) {
                abort(400, 'Filter "'.$param.'" not found');
            }
            if (in_array($param, $arrayProps)) {                
                $queryParams[$param] = guarantee('array', $value);
            }
        }
        return array_merge($defaults, $queryParams);
    }
}

Ворота

class MyGate
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Auth\Access\Response|Void
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
     */
    public function authorizeGetDataCall(User $user, MyRequest $request): Response
    {
        Log::info('[MyGate] Checking permissions …');

        if (in_array(LDAPGroups::Admin, session('PermissionGroups', []))) {
            // no further checks needed
            Log::info('[MyGate] User is administrator. No further checks needed');
            return Response::allow();
        }

        if (
            ($request->has('group') && !in_array(Group::toLDAPGroup($request->get('group')), session('PermissionGroups', []))) ||
            $request->has('owner.department') && !in_array(Department::toLDAPGroup($request->query('owner.department')), session('PermissionGroups', [])) ||
            $request->has('creator.department') && !in_array(Department::toLDAPGroup($request->query('creator.department')), session('PermissionGroups', []))
        ) {
            Log::warning('[MyGate] Access denied due to insufficient group/deparment membership', [ 'group/department' =>
                $request->has('group') ?
                    Group::toLDAPGroup($request->get('group')) :
                ($request->has('owner.department') ?
                    Department::toLDAPGroup($request->query('owner.department')) :
                ($request->has('creator.department') ?
                    Department::toLDAPGroup($request->query('creator.department')) :
                null))
            ]);
            return Response::deny('Access denied');
        }
        if ($request->has('customer_id') || $request->has('article.created_by_id')) {
            $ids = [];
            if ($request->has('customer_id')) {
                $ids = array_merge($ids, $request->query('customer_id'));
            }
            if ($request->has('article.created_by_id')) {
                $ids = array_merge($ids, $request->query('article.created_by_id'));
            }
            $users = User::find($ids);
            $hasOtherLDAPGroup = !$users->every(function($user) {
                return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
            });
            if ($hasOtherLDAPGroup) {
                Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'ids' => $ids ]);
                return Response::deny('Access denied');;
            }
        }
        if ($request->has('owner.login') || $request->has('creator.login')) {
            $logins = [];
            if ($request->has('owner.login')) {
                $logins = array_merge(
                    $logins,
                    guarantee('array', $request->query('owner.login'))
                );
            }
            if ($request->has('creator.login')) {
                $logins = array_merge(
                    $logins,
                    guarantee('array', $request->query('creator.login'))
                );
            }
            $users = User::where([ 'samaccountname' => $logins ])->get();
            $hasOtherLDAPGroup = !$users->every(function($user) {
                return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
            });
            if ($hasOtherLDAPGroup) {
                Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'logins' => $logins ]);
                return Response::deny('Access denied');
            }
        }
        Log::info('[MyGate] Permission checks passed');
        return Response::allow();
    }
}

person shaedrich    schedule 22.03.2021