Grapic dots

Restrict Properties in API Platform with Serialization Groups

Mike Milano

API Platform makes it easy to deliver all properties of an entity, but what about when you want to limit what properties are accessible to either GET or POST/PUT operations?

It’s surprisingly an easy bit of configuration.

What about dynamically limiting access to properties based on something like the user’s role?

This requires creating a service class, but it is still very approachable.

This article will cover:

  • How to limit properties available in GET requests
  • How to limit properties available in POST or PUT requests
  • How to limit properties available based on the current user’s role

Serialization

If you’re no stranger to Symfony, the term Serializer may sound familiar. API Platform uses the Symfony Serializer Component to determine what properties will be available to read and write.

Configuration uses two terms: - Normalization: Refers to reading properties (GET requests) - Denormalization: Refers to writing data (POST/PUT requests)

By default, all properties are avalable for normalization and denormalization.

Serialization Groups

When you require more granular control over the exposed properties, you will be using Serialization Groups. (Referred to as *groups” for the rest of this article)

Groups have other uses, but for this article we will be focusing on how to use them to restrict properties made available through API requests.

User Example

This app allows users to view and update their own profile. It is assumed access to GET and PUT is already restricted by a Doctrine Extension as demonstrated in a previous article.

With that, here are the property level requirements for a user accessing their own profile:

  • name: read and write
  • active: read only
  • roles: no access

In summary, a user can view and update their name. They can view but not update their active status, and they have no access to what roles are assigned to them.

  1. <?php
  2. // api/src/Entity/User.php
  3.  
  4. namespace App\Entity;
  5.  
  6. use ApiPlatform\Core\Annotation\ApiResource;
  7. use Symfony\Component\Serializer\Annotation\Groups;
  8.  
  9. /**
  10.  * @ApiResource(
  11.  * normalizationContext={"groups"={"read"}},
  12.  * denormalizationContext={"groups"={"write"}}
  13.  * )
  14.  */
  15. class User
  16. {
  17. /**
  18.   * @Groups({"read"})
  19.   */
  20. private $active;
  21.  
  22. /**
  23.   * @Groups({"read", "write"})
  24.   */
  25. private $name;
  26.  
  27. /**
  28.   *
  29.   */
  30. private $roles;
  31.  
  32. // ...
  33. }

The result of a GET request to /api/users/1 will not include the roles property.

A PUT request to /api/users/1 will only update the name property, even if active and roles are passed along with it.

Adding Administration Access

Typically your application will require an administrator to de-activate or update the roles of users.

To achieve this, we will:

  • Add new groups to our properties: admin:read, admin:write
  • Create a class to handle dynamically adding support for the 2 new groups
  • Create a service entry to wire up the new class

Let’s first update the User entity class.

  1. <?php
  2. // api/src/Entity/User.php
  3.  
  4. namespace App\Entity;
  5.  
  6. use ApiPlatform\Core\Annotation\ApiResource;
  7. use Symfony\Component\Serializer\Annotation\Groups;
  8.  
  9. /**
  10.  * @ApiResource(
  11.  * normalizationContext={"groups"={"read"}},
  12.  * denormalizationContext={"groups"={"write"}}
  13.  * )
  14.  */
  15. class User
  16. {
  17. /**
  18.   * @Groups({"read", "admin:write"})
  19.   */
  20. private $active;
  21.  
  22. /**
  23.   * @Groups({"read", "write"})
  24.   */
  25. private $name;
  26.  
  27. /**
  28.   * @Groups({"admin:read"})
  29.   */
  30. private $roles;
  31.  
  32. // ...
  33. }

We’ve now added admin:write and admin:read where administration access is desired.

What is left is to add the class to handle injecting the groups when desired, and the service to wire it all up.

Creating the Context Builder

  1. Create the directory: src/Serializer
  2. Create the class file: src/Serializer/AdminContextBuilder.php
  3. Add the source code below to AdminContextBuilder.php

Take a close look at the createFromRequest() method. This is where we are injecting normalization group admin:read, and denormalization group admin:write. It’s being injected if the user is an administrator.

You could further restrict the logic based on the $resourceClass, but here we’ll apply it for all resource classes.

  1. <?php
  2.  
  3. namespace App\Serializer;
  4.  
  5. use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
  8.  
  9. final class AdminContextBuilder implements SerializerContextBuilderInterface
  10. {
  11. private $decorated;
  12. private $authorizationChecker;
  13.  
  14. public function __construct(SerializerContextBuilderInterface $decorated, AuthorizationCheckerInterface $authorizationChecker)
  15. {
  16. $this->decorated = $decorated;
  17. $this->authorizationChecker = $authorizationChecker;
  18. }
  19.  
  20. public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array
  21. {
  22. $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
  23.  
  24. if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
  25. if ($normalization) {
  26. // Add admin:write for normalization.
  27. $context['groups'][] = 'admin:read';
  28. } else {
  29. // Add admin:read contexts for denormalization.
  30. $context['groups'][] = 'admin:write';
  31. }
  32. }
  33.  
  34. return $context;
  35. }
  36. }

The last thing to do is create the service.

Add the following entry into config/services.yaml.

  1. services:
  2. 'App\Serializer\AdminContextBuilder':
  3. decorates: 'api_platform.serializer.context_builder'
  4. arguments: [ '@App\Serializer\AdminContextBuilder.inner' ]
  5. autoconfigure: false

Conclusion

As you can see, a few modifications to the entity file results in a significant amount of control over the properties of an entity. Then by adding another class and service, we were able to dynamically control access based on the current user.

For more information on serializers, head on over to the official docs.

8 Reasons Why Standardizing on Drupal is the Right Choice for Enterprise.
Read about it in this free white paper.

Download for Free Now!