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 includesid
,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.
- Any property defined with the
- For denormalization requests (POST/PUT/etc.), the 'write' context is applied.
- Any property defined with the
write
group can be added/updated.
- Any property defined with the
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 theemail
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.
- Log in to post comments