Skip to content

Commit 660c057

Browse files
DavidBennettUKvincentchalamon
authored andcommitted
Document securing GraphQL fields (including associations) with ApiProperty (#1338)
1 parent 0733c3b commit 660c057

File tree

2 files changed

+108
-2
lines changed

2 files changed

+108
-2
lines changed

client-generator/nextjs.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ Install required dependencies:
2828

2929
$ npx @api-platform/client-generator https://demo.api-platform.com . --generator next --resource book
3030
# Replace the URL by the entrypoint of your Hydra-enabled API
31+
# Omit the resource flag to generate files for all resource types exposed by the API.
3132

32-
> Note: Omit the resource flag to generate files for all resource types exposed by the API.
33+
> Note: On the [API Platform distribution](https://github.com/api-platform/api-platform), you can run
34+
> `generate-api-platform-client` instead of `npx @api-platform/client-generator`.
3335
3436
## Starting the Project
3537

core/graphql.md

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ If you don't know what queries are yet, please [read the documentation about the
176176
For each resource, two queries are available: one for retrieving an item and the other one for the collection.
177177
For example, if you have a `Book` resource, the queries `book` and `books` can be used.
178178

179-
### Global Object Identifier
179+
### Global Object Identifier
180180

181181
When querying an item, you need to pass an identifier as argument. Following the [GraphQL Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm),
182182
the identifier needs to be globally unique. In API Platform, this argument is represented as an [IRI (Internationalized Resource Identifier)](https://www.w3.org/TR/ld-glossary/#internationalized-resource-identifier).
@@ -1132,6 +1132,110 @@ class Book
11321132
}
11331133
```
11341134

1135+
### Securing Properties (Including Associations)
1136+
1137+
You may want to limit access to certain resource properties with a security expression. This can be done with the `ApiProperty` `security` attribute.
1138+
1139+
Note: adding the `ApiProperty` `security` expression to a GraphQL property will automatically make the GraphQL property type nullable (if it wasn't already).
1140+
This is because `null` is returned as the property value if access is denied via the `security` expression.
1141+
1142+
In GraphQL, it's possible to expose associations - allowing nested querying.
1143+
For example, associations can be made with Doctrine ORM's `OneToMany`, `ManyToOne`, `ManyToMany`, etc.
1144+
1145+
It's important to note that the security defined on resource operations applies only to the exposed query/mutation endpoints (e.g. `Query.users`, `Mutation.updateUser`, etc.).
1146+
Resource operation security is defined via the `security` attribute for each operation defined in the `ApiResource` `graphql` attribute.
1147+
This security is *not* applied to exposed associations.
1148+
1149+
Associations can instead be secured with the `ApiProperty` `security` attribute. This provides the flexibility to have different security depending on where an association is exposed.
1150+
1151+
To prevent traversal attacks, you should ensure that any exposed associations are secured appropriately.
1152+
A traversal attack is where a user can gain unintended access to a resource by querying nested associations, gaining access to a resource that prevents direct access (via the query endpoint).
1153+
For example, a user may be denied using `Query.getUser` to get a user, but is able to access the user through an association on an object that they do have access to (e.g. `document.createdBy`).
1154+
1155+
The following example shows how associations can be secured:
1156+
1157+
```php
1158+
<?php
1159+
// api/src/Entity/User.php
1160+
1161+
namespace App\Entity;
1162+
1163+
use ApiPlatform\Core\Annotation\ApiProperty;
1164+
use ApiPlatform\Core\Annotation\ApiResource;
1165+
use Doctrine\ORM\Mapping as ORM;
1166+
1167+
/**
1168+
* @ORM\Entity
1169+
*/
1170+
#[ApiResource(
1171+
graphql: [
1172+
'item_query' => ['security' => 'is_granted("VIEW", object)'],
1173+
'collection_query' => ['security' => 'is_granted("ROLE_ADMIN")'],
1174+
],
1175+
)]
1176+
class User
1177+
{
1178+
// ...
1179+
1180+
/**
1181+
* @ORM\ManyToMany(targetEntity=Document::class, mappedBy="viewers")
1182+
*/
1183+
#[ApiProperty(security: 'is_granted("VIEW", object)')]
1184+
private Collection $viewableDocuments;
1185+
1186+
/**
1187+
* @ORM\Column(type="string", length=180, unique=true)
1188+
*/
1189+
#[ApiProperty(security: 'is_granted("ROLE_ADMIN")')]
1190+
private string $email;
1191+
}
1192+
```
1193+
1194+
```php
1195+
<?php
1196+
// api/src/Entity/Document.php
1197+
1198+
namespace App\Entity;
1199+
1200+
use ApiPlatform\Core\Annotation\ApiProperty;
1201+
use ApiPlatform\Core\Annotation\ApiResource;
1202+
use Doctrine\ORM\Mapping as ORM;
1203+
1204+
/**
1205+
* @ORM\Entity
1206+
*/
1207+
#[ApiResource(
1208+
graphql: [
1209+
'item_query' => ['security' => 'is_granted("VIEW", object)'],
1210+
'collection_query' => ['security' => 'is_granted("ROLE_ADMIN")'],
1211+
],
1212+
)]
1213+
class Document
1214+
{
1215+
// ...
1216+
1217+
/**
1218+
* @ORM\ManyToMany(targetEntity=User::class, inversedBy="viewableDocuments")
1219+
*/
1220+
#[ApiProperty(security: 'is_granted("VIEW", object)')]
1221+
private Collection $viewers;
1222+
1223+
/**
1224+
* @ORM\ManyToOne(targetEntity=User::class)
1225+
*/
1226+
#[ApiProperty(security: 'is_granted("VIEW", object)')]
1227+
protected ?User $createdBy = null;
1228+
}
1229+
```
1230+
1231+
The above example only allows admins to see the full collection of each resource (`collection_query`).
1232+
Users must be granted the `VIEW` attribute on a resource to be able to query it directly (`item_query`) - which would use a `Voter` to make this decision.
1233+
1234+
Similar to `item_query`, all associations are secured, requiring `VIEW` access on the parent object (*not* on the association).
1235+
This means that a user with `VIEW` access to a `Document` is able to see all users who are in the `viewers` collection, as well as the `createdBy` association.
1236+
This may be a little too open, so you could instead do a role check here to only allow admins to access these fields, or check for a different attribute that could be implemented in the voter (e.g. `VIEW_CREATED_BY`.)
1237+
Alternatively, you could still expose the users, but limit the visible fields by limiting access with `ApiProperty` `security` (such as the `User::$email` property above) or with [dynamic serializer groups](serialization.md#changing-the-serialization-context-dynamically).
1238+
11351239
## Serialization Groups
11361240

11371241
You may want to restrict some resource's attributes to your GraphQL clients.

0 commit comments

Comments
 (0)