Expose a REST API to different kinds of users with Api-Platform [Part 3/4]

Benoit POLASZEK
2 min readJun 3, 2019

--

In the 2nd part, we exposed a REST API for our Order entity, which can be reached through a Json Web Token authentication, from an Employee or a Customer. However, how can we prevent a customer to access other customers’ orders? How can we prevent a customer to access the fraudulent property?

Filter results based on user type

Api-Platform provides a powerful mechanism of extensions to hook on Doctrine queries for different operations. We can easily set up an extension that will check the type of operation, the type of resource being queried, and the current user logged in to apply or not some restrictions:

Thanks to Symfony’s autowiring and autoconfiguration, you have nothing else to do. But if you want a fine control of your services, don’t hesitate to declare it in your services.yaml:

# config/services.yaml
services:

App\Api\CustomerOrderACLFilter:
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
- { name: api_platform.doctrine.orm.query_extension.item }

Now, if you request /api/orders authenticated as john@employee, you should see all orders. If you authenticate with bob@customer, you should only see Bob’s ones.

Hide private data to Customers

Until now, we didn’t define any serialization groups, which means every property of an Order and its relations are systematically exposed.

Let’s say we want to expose the order id and its product to both employees and customers, but we want to expose the fraudulent property to employees only.

What we could do is to define a common serialization group as order:read, along with more specific groups: employee:order:read for employees and customer:order:read for customers.

So, our Order entity could look like this (we won’t use customer:order:read here, but it doesn’t matter) :

# src/Entity/Order.php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;

/**
* @ApiResource()
* @ORM\Entity(repositoryClass="App\Repository\OrderRepository")
* @ORM\Table("`order`")
*/
class Order
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*
@Groups({"order:read"})
*/
private $id;

/**
* @ORM\ManyToOne(targetEntity="App\Entity\Customer")
* @ORM\JoinColumn(nullable=false)
*/
private $customer;

/**
* @ORM\Column(type="string", length=255)
*
@Groups({"order:read"})
*/
private $product;

/**
* @ORM\Column(type="boolean")
*
@Groups({"employee:order:read"})
*/
private $fraudulent;

// ...
}

We must tell Api-Platform that read operations must use the order:read group, so edit the @ApiResource annotation:

@ApiResource(normalizationContext={"groups": {"order:read"}})

Now, if you hit /api/orders as john@employee, you’ll only see the id and product properties. To see employee-specific properties, we need to dynamically add the employee: prefix, after ensuring the user is an Employee.

To achieve this, we need to decorate the service that builds the serialization context, by adding the appropriate prefix depending on the user type. A simple decorator like this could do the trick:

Don’t forget to register it in your services.yaml:

# config/services.yaml

services:
App\Api\UserContextBuilder:
decorates: 'api_platform.serializer.context_builder'
arguments: [ '@App\Api\UserContextBuilder.inner' ]

Now, each time a serialization context is built with order:read in its groups, we’ll add employee:order:read or customer:order:read as well, depending on the user logged in. When an employee requests /api/orders, Api-Platform will serialize properties that belong to the order:read and the employee:order:read groups.

Previous | Next

--

--