Development

API Platform: Dynamically Restrict Properties Based on User

When developing an API which has users with different roles accessing the data, there's often properties which should only be accessible to privileged users.

I've written about how to restrict properties, by adding serialization groups to each property, but now we need to set the serialization context based on the current user.

Our API

To explain this, let me define some details about the API we will discuss:

  • There is a Comment resource available for anyone to retrieve.
  • The Comment resource includes id, name, email, and other fields which we're not concerned about here.
  • Everyone can see the id name properties
  • Only administrators can see the email property.

Of course a comment would typically have a message and relation to an entity the comment is associated with, but all I want to explain here is how to make email available to administrators, and no one else.

Here is the relevant code for the Comment entity:

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

    namespace App\Entity;

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

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

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

        /**
         * 
         */
        private $email;

        // ...
    }
...

Notice we've provided the default serialization contexts in the ApiResource() definition.

This means:

  • For normalization requests (GET), the read context is applied.
    • Any property defined with the read group will be included with the results.
  • For denormalization requests (POST/PUT/etc.), the 'write' context is applied.
    • Any property defined with the write group can be added/updated.

I'll focus on the read context and group for the sake of simplicity.

Our objective at this point is to make the email property available to administrators.

The Solution

Fortnately, API Platform provides a way to change the serialization context dynamically.

To make the email property available to administrators, we need to:

  • Add an admin:read serialization group to the email property.
  • Apply the admin:read context when an administrator is making the request.

First, let's update the entity.

<?php
// src/Entity/Comment.php
namespace App\Entity;

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

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

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

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

    // ...
}
...

At this point, there is no change in the results a user will receive. We've defined the admin:read group on the email property, but read and write are the only contexts available by default.

What is required now is to create a context builder and expose it as a service.

Consider createFromRequest() carefully to see how we're adding admin:read and admin:write.

Note: We could target the Comment resource by analyzing $context in createFromRequest(), however I have opted to make admin:read and admin:write available to all resources if the current user is an administrator.

<?php
// src/Serializer/AdminContextBuilder.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);
        // Add `admin:read` for normalization requests
        // Otherwise, add `admin:write` for denormalization requests
        if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
            $context['groups'][] = $normalization ? 'admin:read' : 'admin:write';
        }

        return $context;
    }
}

Now define the service in config/services.yaml

...
    'App\Serializer\AdminContextBuilder':
        decorates: 'api_platform.serializer.context_builder'
        arguments: [ '@App\Serializer\AdminContextBuilder.inner' ]
        autoconfigure: false

... and that's it. Now when a user with ROLE_ADMIN makes a request, admin:read group will be added and any property with that group defined will be made available.

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!