ApiPlatform - implement security authorization on subresource route - symfony

I'm using Symfony5 and ApiPlatform
I have a User entity and a Product entity.
I want to list all my user's products through a subressource, to do so I've implemented my user class as follow :
/**
* #ApiResource(
* attributes={
* "normalization_context"={"groups"={"user:read", "user:list"}},
* "denormalization_context"={"groups"={"user:put", "user:post"}}
* },
* subresourceOperations={
* "api_users_consultations_get_subresource"={
* "method"="GET",
* "security"="is_granted('ROLE_ADMIN')"
* }
* },
* collectionOperations={
* "get"={
* "method"="GET",
* "security"="is_granted('ROLE_ADMIN')",
* "normalization_context"={"groups"={"user:list"}}
* },
* "post"={
* "method"="POST",
* "security_post_denormalize"="is_granted('POST', object)",
* "denormalization_context"={"groups"={"user:post"}}
* }
* },
* itemOperations={
* "get"={
* "method"="GET",
* "security"="is_granted('GET', object)",
* "normalization_context"={"groups"={"user:read"}}
* }
* }
* )
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=false)
* #ORM\Entity(repositoryClass=UserRepository::class)
*/
class User
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
* #Groups({"user:read", "user:list"})
*
*/
private $id;
/**
* #ORM\OneToMany(targetEntity=Product::class, mappedBy="user")
* #ApiSubresource()
*/
private $product;
}
It does create a route /users/{id}/products and return me what I want.
The part where I'm blocking is when I want to add authorization to this route:
ROLE_ADMIN can access this route
ROLE_USER who own the ressource can access it
All the other roles will receive FORBIDDEN
To do so I've followed the doc : https://api-platform.com/docs/core/subresources/#using-serialization-groups
Added subresourceOperations to #ApiSubresource annotation
recovered the name of my grenerated route api_users_consultations_get_subresource through the bin/console debug:router command
and simply set a security=is_granted('ROLE_ADMIN') method like for other operations.
or security=is_granted('SUB_LIST', object) to hit the voter
but when I run my tests I get 200 where I should receive 403, the UserVoter or ProductVoter are not triggered nor the is_granted('ROLE_ADMIN') rule.
As if the subresourceOperations annotation wasn't recognize by the ApiPlatform.
I've also tried changing the name of the operation from api_users_consultations_get_subresource to :
consultations_get_subresource
api_consultations_get_subresource
clients_get_subresource
api_clients_get_subresource
and different other variants as I saw on Github it solved the issue in some cases (like here https://github.com/api-platform/api-platform/issues/1581#issuecomment-662503549) but it has not worked for me.
So I'm wondering is there something I havn't done to implement it correctly ?
Is it a known issue of ApiPlatform ?
Does anyone see where my logic is failing ?
Is there another way to setup security on subresource routes ?
Are there more docs on security realted to subresource? I have not find a lot of material on this particular subject

Related

Mercure doesn't update DELETE method when use SoftDeleatable extensions

For a project, I use Api Platform and Mercure. I currently have a problem with the SoftDeleatable extension. Without this extension, when I delete an entity, Mercure receives the DELETE. But if I use the SoftDeleatable extension, the deletion is done correctly but Mercure does not receive the DELETE.
Code Entity:
/**
* #ORM\Entity(repositoryClass=MatchesRepository::class)
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=false)
* #ApiResource(
* mercure=true,
* attributes={
* "normalization_context"={"groups"="read"},
* "denormalization_context"={"groups"="write"}
* },
* collectionOperations={
* "get",
* "post"={"security"="is_granted('ROLE_USER')"}
* },
* itemOperations={
* "get",
* "put"={"security"="is_granted('ROLE_ADMIN') or object.owner == user"},
* "delete"={
* "security"="is_granted('ROLE_ADMIN') or object.owner == user"
* },
* }
* )
*/
class Matches
{
Just added mercure=true for enable mercure
PS: I doesn't test if change the DELETE function work or not but SoftDeleatable do it normally :/

How to enable pagination for Sub Resource?

In my API config file, I disabled pagination :
api_platform.yaml :
api_platform:
collection:
pagination:
enabled: false
But I want to enable pagination for a specific subresource :
Parent class :
/**
* #ORM\Entity(repositoryClass="App\Repository\ForumRepository")
* #ApiResource(
* collectionOperations={
* "get",
* },
* itemOperations={"get"},
* )
*/
class Forum
{
...
/**
* #var ForumSubject[]|ArrayCollection
*
* #ORM\OneToMany(
* targetEntity="App\Entity\ForumSubject",
* mappedBy="forum",
* )
* #ApiSubresource(maxDepth=1)
*/
private $subjects;
...
Children class :
/**
* #ORM\Entity(repositoryClass="App\Repository\ForumSubjectRepository")
* #ApiResource(
* collectionOperations={
* "post",
* },
* itemOperations={
* "get",
* "put",
* "delete"
* },
* )
*/
class ForumSubject
{
...
Everything works fine I can access my sub-resources :
/api/forums/a21cb5db-aed7-45a6-884f-3d3e6d7abd8c/subjects
(Return all subjects of forum, route name : api_forums_subjects_get_subresource ).
But I am unable to enable paging on this route, the documentation does not mention anything.
After research I tried this, but nothing works :
Does not work in parent or children class :
* #ApiResource(
* ...
* subresourceOperations={
* "api_forums_subjects_get_subresource"={"pagination_enabled"=true}
* }
Does not work in parent class ; error un children class (The operation "api_forums_subjects_get_subresource" cannot be found in the Swagger specification).
* #ApiResource(
* ...
* collectionOperations={
* "get",
* "api_forums_subjects_get_subresource"={"pagination_enabled"=true}
* },
After looking in the API Platform source code, here is the solution :
In children class :
* #ApiResource(
* ...
* collectionOperations={
* ...
* "api_forums_subjects_get_subresource"={
* "method"="get", // don't forget : otherwise trigger a strange swagger error
* "pagination_enabled"=true
* }

How can I change api-platform {id} to a different name?

I am using api-platform with Symfony 4. It works fine, but I would like to change a GET url from:
/booking/{id} to /booking/{bookingId}
I'm using my own DTO object and custom data provider (not Doctrine ORM).
Here are the current #ApiResource and #ApiProperty definitions that work fine:
/**
*
* #ApiResource(
* itemOperations={
* "get"={
* "path"="/booking/{id}",
* },
* "api_bookings_get_item"={
* "swagger_context"={
* "operationId"="getBookingItem",
* "summary"="Retrieves details on a booking",
* "parameters"= {
* {
* "name"="id",
* "description"="Booking ID",
* "default"="15000",
* "in"="path",
* "required"=true,
* "type"="string"
* }
* },
* "responses"={
* "200"={
* "description"="Results retrieved"
* },
* "404"={
* "description"="Booking not found"
* }
* }
* }
* }
* },
* collectionOperations={}
* )
*/
final class Booking
{
/**
* #var string
* #Assert\NotBlank
*
* #ApiProperty(
* identifier=true,
* attributes={
* "swagger_context"={
* "description"="Booking ID",
* "required"=true,
* "type"="string",
* "example"="123456"
* }
* }
* }
*/
public $id;
// other variables
}
However, if I change all the references from 'id' to 'bookingId' it stops working and I get a 404 error. Here are the changes I made to the above code:
"path"="/booking/{bookingId}"
"name"="bookingId"
public $bookingId;
Is api-platform hard-coded to use 'id' as an identifier? Is there any way to change this?
In Api-platform the id parameter is hardcoded:
namespace ApiPlatform\Core\DataProvider;
private function extractIdentifiers(array $parameters, array $attributes)
{
if (isset($attributes['item_operation_name'])) {
if (!isset($parameters['id'])) {
throw new InvalidIdentifierException('Parameter "id" not found');
}
but you can create your own operation and use the parameter name that you want there is a great example in docs custom operations
You can customize the apiResource identifier via:
/**
* #ApiProperty(identifier=false)
*/
private $id;
and:
/**
* #ApiProperty(identifier=true)
*/
private $uuid;
where uuid is the new identifier to be used in request URLs.
For reference: symfonycasts did an excellent tutorial here:
https://symfonycasts.com/screencast/api-platform-extending/uuid-identifier

Annotation to show Entity's property description - using nelmioapidocbundle

I am using nelmioapidocbundle in order to documents my Rest API built on the top of symfony-2.x.
I can't found the right annotation to use to show each Entity's property description on the return section (Please see bellow attached image).
My Entity :
/**
* Checkins
*
* #ORM\Table(name="CheckIns")
* #ORM\Entity(repositoryClass="Project1\ApiBundle\Entity\CheckinsRepository")
*
* #ExclusionPolicy("none")
*/
class Checkins
{
/**
* #var integer
*
* #ORM\Column(name="id", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #Groups({"checkin"})
* #
*/
private $id;
My Controller :
class CheckinController extends BaseRestController
{
/**
* #ApiDoc(
* resource=true,
* description="Find checkin by ID",
*
* parameters={
* {"name"="categoryId", "dataType"="integer", "required"=true, "description"="checkin id"}
* }
*
* output={
* "class"="Project1\ApiBundle\Entity\Checkins",
* "groups"={"checkin"}
* },
* statusCodes={
* 200="Checkin found",
* 400="ID is required",
* 404="Checkin not found"
* }
* )
*
* #Rest\View()
*/
public function getAction(Request $request)
{}
Result ( Description column is empty ) :
There's a description in doc section of the bundle:
For classes parsed with JMS metadata, description will be taken from the properties doc comment, if available.
For Form Types, you can add an extra option named description on each
field
Please visit the following link for more instructions(info at the bottom of the section):
https://github.com/nelmio/NelmioApiDocBundle/blob/master/Resources/doc/index.md#the-apidoc-annotation

FOSRestBundle: routes and annotations for parameters

I'm able to get GET parameters with #QueryParam() annotation, but it looks like it works for Query String data only: /user?id=123.
I'd prefer to have it like /user/123 instead. For this, I might use #Get("/user/{id}") annotation, but I don't see it has additional metadata which #QueryParam() has:
name="id", requirements="\d+", default="1", description="User id"
If I use both of the annotations, I get an error:
ParamFetcher parameter conflicts with a path parameter 'id' for route 'getone'
My conflicting docblock:
/**
* Finds and displays a Users entity.
*
* #Rest\View
* #Rest\Get("/user/{id}")
* #Rest\QueryParam(name="id", requirements="\d+", default="1", description="User id")
* #ApiDoc(section="Partner Users")
* #param int $id
* #return array
*/
PS I need to have an id in the path (/user/123), not in query, and I also need to use #QueryParam() as it's read by NelmioApiDocBundle. How may I resolve this issue?
FOSRestBundle's #Get annotation extends FOSRestBundle's #Route which in turn extends SensioFrameworkExtraBundle's #Route.
Have a look at the code and see the documentation chapter #Route and #Method.
The requirements and defaults attributes expect an array.
/**
* #Rest\View
* #Rest\Get("/user/{id}", requirements={"id" = "\d+"}, defaults={"id" = 1})
* #ApiDoc(
* description="Returns a User Object",
* parameters={
* {"name"="id", "dataType"="integer", "required"=true, "description"="User Id"}
* }
* )
*/
public function getAction($id)
{
// ...
}
If you want a description for requirements just do this in your annotation
/**
* #Rest\View
* #Rest\Get("/user/{id}", requirements={"id" = "\d+"}, defaults={"id" = 1})
* #ApiDoc(
* description="Returns a User Object",
* requirements={
* {"name"="id", "dataType"="integer", "required"=true, "description"="User Id"}
* }
* )
*/

Resources