Development

Restrict Properties in API Platform with Serialization Groups

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.

<?php
// api/src/Entity/User.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"read"}},
 *     denormalizationContext={"groups"={"write"}}
 * )
 */
class User
{
    /**
     * @Groups({"read"})
     */
    private $active;

    /**
     * @Groups({"read", "write"})
     */
    private $name;

    /**
     * 
     */
    private $roles;

    // ...
}

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.

<?php
// api/src/Entity/User.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;

/**
 * @ApiResource(
 *     normalizationContext={"groups"={"read"}},
 *     denormalizationContext={"groups"={"write"}}
 * )
 */
class User
{
    /**
     * @Groups({"read", "admin:write"})
     */
    private $active;

    /**
     * @Groups({"read", "write"})
     */
    private $name;

    /**
     * @Groups({"admin:read"})
     */
    private $roles;

    // ...
}

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.

<?php

namespace App\Serializer;

use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
use Symfony\Component\HttpFoundation\Request;
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')) {
            if ($normalization) {
                // Add admin:write for normalization.
                $context['groups'][] = 'admin:read';
            } else {
                // Add admin:read contexts for denormalization.
                $context['groups'][] = 'admin:write';
            }
        }

        return $context;
    }
}

The last thing to do is create the service.

Add the following entry into config/services.yaml.

services:
    'App\Serializer\AdminContextBuilder':
        decorates: 'api_platform.serializer.context_builder'
        arguments: [ '@App\Serializer\AdminContextBuilder.inner' ]
        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.

Up Next

Ready To Get Started?

Schedule a complimentary 30-minute strategy consultation with one of our Drupal experts as early as today. We promise...they don't bite!