Skip to content
Merged
2 changes: 1 addition & 1 deletion core/data-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,4 @@ services:

Previous chapter: [Extending JSON-LD context](extending-jsonld-context.md)

Next chapter: [Security](security.md)
Next chapter: [Extensions](extensions.md)
169 changes: 169 additions & 0 deletions core/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Extensions

API Platform Core provides a system to extend queries on items and collections.

Extensions are specific to Doctrine, and therefore, the Doctrine ORM support must be enabled to use this feature. If you use custom providers it's up to you to implement your own extension system or not.

## Custom Extension

Custom extensions must implement the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface` and / or the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface` interfaces, to be run when querying for a collection of items and when querying for an item respectively.

If you use [custom data providers](data-providers.md), they must support extensions and be aware of active extensions to work properly.

## Example
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should really be a level 2 title?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What level would you use ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exemple

blabla

Exemple

blabla maybe ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i prefer ### Example @Simperfit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if the title in the file /index.md is Filter upon the current user then it is necessary to put ## Filter upon the current user instead of ## Example.

And change anchor in the file /index.md.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index.md has a "Filter upon the current user" chapter, because it is what it is, it suits better than "Example" when that what a user wants to do, he finds it instantly. that's why the "Filter upon the current user" is linked with the example anchor (By the way, it used to be a "filter upon the current user" anchor, but @dunglas made me change it for "Example". For the anchor level, since it appear on the summary, I thought it had to be the same level than the previous anchor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


In the following example, we will see how to always get the offers owned by the current user. We will set up an exception, whenever the user has the `ROLE_ADMIN`.

Given these two entities:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same concern about formatting here... (and many other places below)


```php
<?php
// src/AppBundle/Entity/User.php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;

/**
* @ApiResource
*/
class User
{
// ...
}
```

```php
<?php
// src/AppBundle/Entity/Offer.php

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiResource;

/**
* @ApiResource
*/
class Offer
{
/**
* @var User
* @ORM\ManyToOne(targetEntity="User")
*/
private $user;

//...
}
```

```php
<?php
// src/AppBundle/Doctrine/ORM/Extension/CurrentUserExtension.php

namespace AppBundle\Doctrine\ORM\Extension;

use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use AppBundle\Entity\Offer;
use AppBundle\Entity\User;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query\Expr;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;

final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@api-platform/core-team IIUC the docs should contain best practices, is it one to implements both in one class ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to me for this use case. Creating two classes with duplicated code isn't ok however. So I would keep it as is.

{
private $authorizationChecker;
private $propertyNameCollectionFactory;
private $propertyMetadataFactory;
private $token;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, TokenStorageInterface $token, AuthorizationChecker $checker)
{
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->token = $token;
$this->authorizationChecker = $checker;
}

/**
* {@inheritdoc}
*/
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
$this->addWhere($queryBuilder, $resourceClass);
}

/**
* {@inheritdoc}
*/
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, string $operationName = null)
{
$this->addWhere($queryBuilder, $resourceClass);
}

/**
*
* @param QueryBuilder $queryBuilder
* @param string $resourceClass
*/
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass)
{
$user = $this->token->getToken()->getUser();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should test if $user is an instance of User because IIRC Symfony will return the string 'anon.' when the user isn't logged in.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively you can add a firewall rule to check that the user is connected.

if ($user instanceof User && Offer::class === $resourceClass && !$this->authorizationChecker->isGranted('ROLE_ADMIN')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to do something if $user is not an instance of User: adding something like where 1=2 to the query or (better) mention that the security component must be used to require a user to be authenticated to access this route.

$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.user = :current_user', $rootAlias));
$queryBuilder->setParameter('current_user', $user->getId());
}
}
}

```

Finally register the custom extension:

```yaml
# app/config/services.yml

services:
app.doctrine.orm.query_extension.current_user:
class: AppBundle\Doctrine\ORM\Extension\CurrentUserExtension
public: false
arguments:
- '@api_platform.metadata.property.name_collection_factory'
- '@api_platform.metadata.property.metadata_factory'
- '@security.token_storage'
- '@security.authorization_checker'
tags:
- { name: api_platform.doctrine.orm.query_extension.collection, priority: 9 }
- { name: api_platform.doctrine.orm.query_extension.item }
```

Thanks to the `api_platform.doctrine.orm.query_extension.collection` tag, API Platform will register this service as a collection extension. The `api_platform.doctrine.orm.query_extension.item` do the same thing for items.

Notice the priority level for the `api_platform.doctrine.orm.query_extension.collection` tag. When an extension implements the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultCollectionExtensionInterface` or the `ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryResultItemExtensionInterface` interface to return results by itself, any lower priority extension will not be executed. Because the pagination is enabled by default with a priority of 8, the priority of the `app.doctrine.orm.query_extension.current_user` service must be at least 9 to ensure its execution.

### Blocking Anonymous Users

This example adds a `WHERE` clause condition only when a fully authenticated user without `ROLE_ADMIN` tries to access to a resource. It means that anonymous users will be able to access to all data. To prevent this potential security issue, the API must ensure that the current user is authenticated.

To secure the access to endpoints, use the following access control rule:

```yaml
# app/config/security.yml

security:
# ...

access_control:
# ...
- { path: ^/offers, roles: IS_AUTHENTICATED_FULLY }
- { path: ^/users, roles: IS_AUTHENTICATED_FULLY }
```

Previous chapter: [Data Providers](data-providers.md)

Next chapter: [Security](security.md)
2 changes: 1 addition & 1 deletion core/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ For instance, if you wish to restrict the access of some endpoints, you can use
It is also possible to use the [event system](events.md) for more advanced logic or even [custom actions](operations.md#creating-custom-operations-and-controllers)
if you really need to.

Previous chapter: [Data Providers](data-providers.md)
Previous chapter: [Extensions](extensions.md)

Next chapter: [Performance](performance.md)
2 changes: 1 addition & 1 deletion core/serialization-groups-and-relations.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ final class BookContextBuilder implements SerializerContextBuilderInterface
{
$context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes);
$subject = $request->attributes->get('data');

if ($subject instanceof Book && $this->authorizationChecker->isGranted('ROLE_ADMIN') && false === $normalization) {
$context['groups'][] = 'admin_input';
}
Expand Down
17 changes: 10 additions & 7 deletions index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,27 @@
13. [Data Providers](core/data-providers.md)
1. [Custom Collection Data Provider](core/data-providers.md#creating-a-custom-data-provider#custom-collection-data-provider)
2. [Custom Item Data Provider](core/data-providers.md#returning-a-paged-collection#custom-item-data-provider)
14. [Security](core/security.md)
15. [Performance](core/performance.md)
14. [Extensions](core/extensions.md)
1. [Custom Extension](core/extensions.md#custom-extension)
2. [Filter upon the current user](core/extensions.md#example)
16. [Security](core/security.md)
17. [Performance](core/performance.md)
1. [Enabling the Metadata Cache](core/performance.md#enabling-the-metadata-cache)
2. [Using PPM (PHP-PM)](core/performance.md#using-ppm-php-pm)
3. [Doctrine Queries and Indexes](core/performance.md#doctrine-queries-and-indexes)
1. [Search Filter](core/performance.md#search-filter)
2. [Unserialized Properties Hydratation](core/performance.md#unserialized-properties-hydratation)
16. [Operation Path Naming](core/operation-path-naming.md)
18. [Operation Path Naming](core/operation-path-naming.md)
1. [Configuration](core/operation-path-naming.md#configuration)
2. [Create a Custom Operation Path Naming](core/operation-path-naming.md#create-a-custom-operation-path-resolver)
1. [Defining the Operation Path Naming](core/operation-path-naming.md#defining-the-operation-path-resolver)
2. [Registering the Service](core/operation-path-naming.md#registering-the-service)
3. [Configure it](core/operation-path-naming.md#configure-it)
17. [Accept `application/x-www-form-urlencoded` Form Data] (core/form-data.md)
18. [FOSUserBundle Integration](core/fosuser-bundle.md)
19. [Accept `application/x-www-form-urlencoded` Form Data] (core/form-data.md)
20. [FOSUserBundle Integration](core/fosuser-bundle.md)
1. [Creating a `User` Entity with Serialization Groups](core/fosuser-bundle.md#creating-a-user-entity-with-serialization-groups)
19. [NelmioApiDocBundle integration](core/nelmio-api-doc.md)
20. [AngularJS Integration](core/angularjs-integration.md)
21. [NelmioApiDocBundle integration](core/nelmio-api-doc.md)
22. [AngularJS Integration](core/angularjs-integration.md)
1. [Restangular](core/angularjs-integration.md#restangular)
2. [ng-admin](core/angularjs-integration.md#ng-admin)

Expand Down