How to set normalization groups based on user's role and request method

删除回忆录丶 提交于 2020-03-04 20:01:10

问题


I am creating a sandbox app as Api-platform practice and I have the following problem to address:
Let's consider following REST endpoint for user entity:
DISCLAIMER in the code examples there are a little more attributes but the whole concept applies regarding that

Collection-get(aka. /api/users) - only available for admin users(all attributes available, maybe we exclude hashed password)

POST - everyone should have access to following attributes: username, email, plainPassword(not persisted just in case someone asks)

PATCH/PUT - here it becomes quite tricky: I want those with ROLE_ADMIN to have access to username, email, plainPassword fields. And those who are the owners to only be able to alter plainPassword

DELETE - only ROLE_ADMIN and owners can delete

I will start with the resource config

resources:
    App\Entity\User:
        # attributes:
        #     normalization_context:
        #         groups: ['read', 'put', 'patch', 'post', 'get', 'collection:get']
        #     denormalization_context:
        #         groups: ['read', 'put', 'patch', 'post', 'get', 'collection:get']
        collectionOperations:
            get:
                security: 'is_granted("ROLE_ADMIN")'
                normalization_context: { groups: ['collection:get'] }
            post: 
                normalization_context: { groups: ['admin:post', 'post'] }
        itemOperations:
            get:
                normalization_context: { groups: ['admin:get', 'get'] }
                security: 'is_granted("ROLE_ADMIN") or object == user'
            put:
                normalization_context: { groups: ['admin:put', 'put'] }
                security: 'is_granted("ROLE_ADMIN") or object == user'
            patch:
                normalization_context: { groups: ['admin:patch', 'patch'] }
                security: 'is_granted("ROLE_ADMIN") or object == user'
            delete:
                security: 'is_granted("ROLE_ADMIN") or object == user'

Here is the serializer config

App\Entity\User:
    attributes:
        username:
            groups: ['post', 'admin:put', 'admin:patch', 'collection:get', 'get']
        email:
            groups: ['post', 'admin:put', 'admin:patch', 'collection:get', 'get']
        firstName:
            groups: ['post', 'admin:put', 'admin:patch', 'collection:get', 'get']
        lastName:
            groups: ['post', 'admin:put', 'admin:patch', 'collection:get', 'get']
        plainPassword:
            groups: ['post', patch]
        createdAt:
            groups: ['get', 'collection:get']
        lastLoginDate:
            groups: ['get', 'collection:get']
        updatedAt:
            groups: ['collection:get']

Here is the context group builder(Registered as service as it's stated in API-platform doc

<?php

namespace App\Serializer;

use Symfony\Component\HttpFoundation\Request;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class AdminContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

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

    public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
    {
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);

        if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            switch($request->getMethod()) {
                case 'GET':
                    $context['groups'][] = 'admin:get';
                    break;
                case 'POST':
                    $context['groups'][] = 'admin:post';
                case 'PUT':
                    $context['groups'][] = 'admin:put';
                case 'PATCH':
                    $context['groups'][] = 'admin:patch';
            }
        }

        return $context;
    }
}

The issue is that even if I'm logged as a user with only ROLE_USER I am still able to alter username field which should be locked according to the admin:patch normalization group. I am pretty new to the api-platform and I can't quite understand why this does not work but I guess there will be an issue with the context builder. Thanks for your help I'll keep the question updated if I come up with something in the meantime


回答1:


After investigating the docs and browsing youtube and most of all experimenting with the aformentioned user resource I came up with the solution
Let's start with the configuration again:

resources:
    App\Entity\User:
        collectionOperations:
            get:
                security: 'is_granted("ROLE_ADMIN")'
                normalization_context: { groups: ['collection:get'] }
                denormalization_context: { groups: ['collection:get'] }
            post:
                normalization_context: { groups: ['post'] }
                denormalization_context: { groups: ['post'] }
        itemOperations:
            get:
                normalization_context: { groups: ['get'] }
                security: 'is_granted("ROLE_ADMIN") or object == user'
            patch:
                normalization_context: { groups: ['patch'] }
                denormalization_context: { groups: ['patch'] }
                security: 'is_granted("ROLE_ADMIN") or object == user'
            delete:
                security: 'is_granted("ROLE_ADMIN") or object == user'

The main difference between the starting point is that the admin actions should never be stated in the operation groups because they will be added by default to the context.

Next the property groups where we define all the operations available on certain property

App\Entity\User:
    attributes:
        id:
            groups: ['get', 'collection:get']
        username:
            groups: ['post', 'admin:patch', 'get', 'collection:get']
        email:
            groups: ['post', 'admin:patch', 'get', 'collection:get']
        plainPassword:
            groups: ['post', 'patch', 'collection:get']
        firstName:
            groups: ['post', 'patch', 'get', 'collection:get']
        lastName:
            groups: ['post', 'get', 'collection:get']
        createdAt:
            groups: ['get', 'collection:get']
        lastLoginDate:
            groups: ['get', 'collection:get']
        updatedAt:
            groups: ['collection:get']

This is fairly the same from as in the question only thing we need to configure is which actions require to be the 'admin' this can be changed according to your needs whatever you program a blog, library, store or whatever and need some custom actions per role on your API.

At last is the custom context builder

<?php

namespace App\Serializer;

use Symfony\Component\HttpFoundation\Request;
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

final class AdminContextBuilder implements SerializerContextBuilderInterface
{
    private $decorated;
    private $authorizationChecker;

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

    public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
    {
        $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);

        if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            $context['groups'][] = 'admin:patch';
            $context['groups'][] = 'admin:post';
            $context['groups'][] = 'admin:get';
        }

        return $context;
    }
}

This is fairly simple and can be extended on your personal needs basically we check if the current user is an admin give him the properties from groups a, b, c etc. This can also be specified per entity(more on that you can find in API platform doc BookContextBuilder fairly simple

I am pretty sure this is the bread and butter anyone will ever need when building some simple or even complex API where roles will determine who can do what. If this answer will help you pleas be sure to up my answer thanks a lot and happy coding!



来源:https://stackoverflow.com/questions/60340261/how-to-set-normalization-groups-based-on-users-role-and-request-method

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!