Группы динамической сериализации в пакете FOS REST

В настоящее время я использую FOSRESTBundle с JMSSerialize для создания RESTFull API (конечно же).

Мой проект — это экстранет для клиентов и администраторов.

Таким образом, я должен отключить просмотр некоторых полей клиентом, видимый только для администраторов.

Я начал с создания этой конфигурации сериализатора для объекта:

AppBundle\Entity\IncidentComment:
    exclusion_policy: ALL
    properties:
        id:
            expose: true
            groups: [list, details]
        author:
            expose: true
            groups: [list, details]
        addedAt:
            expose: true
            groups: [list, details]
        content:
            expose: true
            groups: [details]
        customerVisible:
            expose: true
            groups: [list_admin, details_admin]

Как видите, группы customerVisible имеют суффикс _admin. Это поле должно отображаться только для администраторов.

Я хочу динамически добавлять группы с суффиксом _admin для установки групп в представлениях, если у пользователя есть, например, роль ROLE_ADMIN или другое условие, не записывая ее при каждом действии каждого из остальных контроллеров.

Я думал о создании настраиваемого обработчика представления. с аргументом контекста безопасности для добавления группы, но я не знаю, правильно ли это.

Как вы думаете, это хороший способ? Есть ли у вас какие-либо предложения по этому поводу?

Кстати, если у кого-то из разработчиков была такая же проблема, я буду рад рассказать, как он ее решил! :)

Спасибо.


person Soullivaneuh    schedule 11.03.2015    source источник
comment
Привет Soullivaneuh, я сейчас столкнулся с точно такой же проблемой. Было бы здорово, если бы был способ добавить группу в сериализатор во время выполнения в контроллере.   -  person Thomas Spycher    schedule 12.03.2015


Ответы (4)


Я только что нашел простой способ добавить SerializerGroups во время выполнения:

private function determineRolebasedSerializerGroup($role, $groupToAdd, Request $request) {
    if (!$this->get('security.context')->isGranted($role))
        return;

    $groups = $request->attributes->get('_view')->getSerializerGroups();
    $groups[] = $groupToAdd;
    $x = $request->attributes->get('_view')->setSerializerGroups($groups);
}

Я добавил этот метод в свой контроллер. Теперь я могу назвать это так:

/**
 * @REST\View(serializerGroups={"company"})
 */
public function getCompanyAction(Company $company, Request $request) {
    $this->determineRolebasedSerializerGroup('ROLE_ADMIN', 'company-admin', $request);

    return $company;
}

Что добавляет группу "company-admin" в группу сериализатора, если текущий пользователь имеет роль "ROLE_ADMIN". Это работает очень хорошо для меня.

person Thomas Spycher    schedule 12.03.2015
comment
Хорошая идея! Но проблема та же, мы всегда должны вызывать его на каждом контроллере. Любой способ добавить его, например, на слушателя? И какой слушатель? - person Soullivaneuh; 12.03.2015
comment
атрибуты-›get('_view) возвращают ноль - person julestruong; 11.08.2016
comment
@julestroong используйте _template вместо _view (для symfony 4 и выше) - person Tanktiger; 16.04.2020

Если вы хотите сделать это с помощью слушателя, вы можете создать свой собственный ViewResponseListener и подписаться на событие kernel.view. Ваш прослушиватель должен запускаться после прослушивателя FOSRest, поэтому вам нужно установить приоритет 101.

app.event.listener.extended_view_response:
    class: AppBundle\EventListener\ExtendedViewResponseListener
    arguments: ["@security.authorization_checker"]
    tags:
        - { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 101 }

РасширенныйViewResponseListener.php

public function onKernelView(GetResponseForControllerResultEvent $event)
{
    if (null !== $viewAttribute = $event->getRequest()->attributes->get('_template')) {
        $groups = [];

        foreach(User::getPossibleRoles() as $role) {
            if ($this->authorizationChecker->isGranted($role)) {
                $groups[] = strtolower(str_replace('ROLE_', '', $role)); // ROLE_USER => user group
            }
        }

        $viewAttribute->setSerializerGroups($groups);
    }

}

И, пожалуйста, не забудьте включить атрибут _template в контроллере, я имею в виду @\FOS\RestBundle\Controller\Annotations\View() в аннотации контроллера. Если вы хотите понять, как это работает, пожалуйста, проверьте ViewResponseListener.php в FosRestBundle.

Второй способ сделать это - ваш пользовательский сериализатор tokenstorageaware_serializer

person Vladislav Kopaygorodsky    schedule 13.01.2016
comment
Это работает? Слушатель FOSRest выполняет сериализацию, поэтому, если этот обработчик запустится после него, будет слишком поздно устанавливать параметры сериализатора, потому что он уже преобразован в Response. И если вы вызовете его раньше, вам придется самостоятельно создать объект View, который будет передан слушателю FOSRest. - person Malvineous; 23.05.2018
comment
Ну, я проверил это, и удивительно, что это работает отлично! Хотя я не уверен, как... Но, как уже упоминалось, вам нужна аннотация @View для всех функций контроллера, чтобы его можно было вызвать, что является небольшим недостатком. - person Malvineous; 23.05.2018
comment
Вы можете настроить свое приложение, чтобы избежать использования аннотации @View каждый раз с прослушивателем событий. - person Vladislav Kopaygorodsky; 25.05.2018

В дополнение к ответу @Vladislav Kopaygorodsky эти дополнения позволяют ему работать, даже если вы опускаете аннотацию @View из функции в контроллере:

namespace AppBundle\EventListener;

use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use AppBundle\Security\User;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use FOS\RestBundle\Controller\Annotations\View as ViewAnnotation;

/**
 * Listener to automatically adjust serializer groups based on user roles.
 *
 * If a user is granted the USER_XYZ role, then this function will add the
 * serializer group "has_role_xyz" before the automatic serialization takes
 * place on the data returned from a controller.
 */
class PermissionResponseListener
{
    private $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    public function onKernelView(GetResponseForControllerResultEvent $event)
    {
        $attr = $event->getRequest()->attributes;
        if (null === $viewAttribute = $attr->get('_template')) {
            // No @Rest\View annotation, create a blank one.
            $viewAttribute = new ViewAnnotation(array());
            $viewAttribute->setPopulateDefaultVars(false);
            $attr->set('_template', $viewAttribute);
        }

        $groups = $viewAttribute->getSerializerGroups();
        // Always include this group, since the default value set in
        // config.yml is no longer used.
        $groups[] = 'Default';

        foreach (User::getPossibleRoles() as $role) {
            if ($this->authorizationChecker->isGranted($role)) {
                $groups[] = 'has_' . strtolower($role); // ROLE_USER => has_role_user
            }
        }

        $viewAttribute->setSerializerGroups($groups);
    }
}

Класс User имеет функцию, которая просто перечисляет все доступные роли:

public static function getPossibleRoles()
{
    return [
        'ROLE_ADMIN',     // system administrators
        'ROLE_OFFICE',    // data entry staff
        'ROLE_USER',      // anyone logged in
    ];
}

И services.yml:

# Set serializer groups based on user roles
AppBundle\EventListener\PermissionResponseListener:
    public: false
    tags:
        - { name: kernel.event_listener, event: kernel.view, method: onKernelView, priority: 101 }

В объекте теперь можно использовать такие аннотации, как:

class ExampleEntity {
    /**
     * @Serializer\Groups({"has_role_admin"})
     */
    protected $adminOnlyValue;
person Malvineous    schedule 23.05.2018

Дополняя ответ Владислава Копайгородского выше и отвечая на вопрос, заданный Мальвинеусом:

Согласно документации Symfony (5.2), приоритет слушателя/подписчика событий является целым числом. Чем выше значение, тем раньше он запускается. На момент написания этой статьи ViewResponseListener пакета FOS Rest установил для себя значение 30. Таким образом, если пользовательский прослушиватель/подписчик имеет приоритет 101, он будет запущен ДО FOS.

person user135649    schedule 21.01.2021