Dynamic serialization groups on FOS REST bundle
Asked Answered
M

4

6

I'm currently using FOSRESTBundle with JMSSerialize to make a RESTFull API (of course).

My project is an extranet for customers and administrators.

In this way, I have to disable some field from being viewed by customer, only visible for administrators.

I started by made this serializer configuration for an entity:

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]

As you can see, customerVisible groups have _admin suffix. This field should be shown only for administrators.

I want to dynamically add groups with _admin suffix to set groups on views if user has, for example, a ROLE_ADMIN role or another condition without write it on each action of each rest controllers.

I was thinking about create a custom view handler with security context argument to add group, but I don't know if is the proper way.

Do you think is the good way? Have you any suggestions about it?

Btw, if some dev had the same problematic, I will be glad to here how he solved it! :)

Thanks.

Matadi answered 11/3, 2015 at 14:51 Comment(1)
Hi Soullivaneuh, i'm currently faced with exactly the same issue. It would be awesome if there is a way to add an group to the serializer during runtime in the controller.Rout
R
4

I've just figured out an easy way to add SerializerGroups during runtime:

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);
}

I've added this method to my controller. I'm now able to call it this way:

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

    return $company;
}

Which adds the group "company-admin" to the serializer group if the current user has the role "ROLE_ADMIN". This works pretty good for me.

Rout answered 12/3, 2015 at 10:9 Comment(3)
Nice idea! But the problem is the same, we always have to call it on each controller. Any way to add it on a listener for example? And which listener?Matadi
attributes->get('_view) return nullIngres
@julestruong use _template instead of _view (for symfony 4 and up)Adrenalin
G
2

If you want to do it with a listener, you can create your own ViewResponseListener and subscribe it to event kernel.view. Your listener must be fired after FOSRest listener, so, you have to set 101 priority.

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 }

ExtendedViewResponseListener.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);
    }

}

And, please, don't forget to enable _template attribute in controller, I mean @\FOS\RestBundle\Controller\Annotations\View() in controller annotation. If you want to figure out how it works - pls check ViewResponseListener.php in FosRestBundle.

The second way to do it - your custom serializer tokenstorageaware_serializer

Godding answered 13/1, 2016 at 23:48 Comment(3)
Does this work? The FOSRest listener performs the serialisation, so if this handler runs after it, it will be too late to set the serialiser options because it's already been converted to a Response. And if you call it before, you have to create the View object yourself that gets passed to the FOSRest listener.Pacemaker
Well I tested it and amazingly it works perfectly! Although I'm not sure how... But as mentioned, you need the @View annotation on all the controller functions in order for it to be called, which is a bit of a drawback.Pacemaker
You can configure your app to avoid using @View annotation every time with event listener.Godding
P
1

Further to the response by @Vladislav Kopaygorodsky, these additions allow it to work even if you omit the @View annotation from the function in the controller:

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);
    }
}

The User class has a function that just lists all available roles:

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

And 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 }

In the entity you can now use annotations like:

class ExampleEntity {
    /**
     * @Serializer\Groups({"has_role_admin"})
     */
    protected $adminOnlyValue;
Pacemaker answered 23/5, 2018 at 6:39 Comment(0)
P
0

Supplmenting Vladislav Kopaygorodsky's answer above and answering the question Malvineous asked:

According to Symfony (5.2) documentation, event listener/subscriber priority is an integer. The higher the value, the earlier it runs. As of this writing, FOS Rest bundle's ViewResponseListener set itself to 30. So if the custom listener/subscriber has priority 101, it will be run BEFORE the FOS one.

Prang answered 21/1, 2021 at 19:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.