Skip to content

Commit 6f7c564

Browse files
committed
doc(security): clarify ApiProperty security behavior during write operations
Explains that `object` is `null` in `security` during PATCH/PUT by design, and that `securityPostDenormalize` should be used for object-aware checks. Fixes api-platform/core#5755
1 parent 806e55c commit 6f7c564

File tree

1 file changed

+62
-0
lines changed

1 file changed

+62
-0
lines changed

symfony/security.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,68 @@ Access control checks in the `security` attribute are always executed before the
125125
`object` doesn't contain the value submitted by the user, but values currently stored in
126126
[the persistence layer](../core/state-processors.md).
127127

128+
## Property-Level Security on Write Operations
129+
130+
When `security` is used on `#[ApiProperty]`, the behavior during write operations (PATCH, PUT) differs
131+
from what you might expect based on the operation-level behavior described above.
132+
133+
During deserialization, `security` on a property is evaluated to build the list of allowed attributes
134+
**before the object is populated with incoming data**. At that point, no object instance is available
135+
yet, so the `object` variable is always `null`. This is by design: `security` on `ApiProperty` acts
136+
as a static, context-free gate — it answers "is this user allowed to interact with this field at all?"
137+
independently of the current object state.
138+
139+
```php
140+
<?php
141+
// api/src/Entity/Book.php
142+
namespace App\Entity;
143+
144+
use ApiPlatform\Metadata\ApiProperty;
145+
146+
class Book
147+
{
148+
// object is null during PATCH/PUT — use only role checks or user checks here
149+
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]
150+
private ?string $adminOnlyField = null;
151+
}
152+
```
153+
154+
If your access logic depends on the object's current state (for example, to restrict changes based
155+
on ownership or a previous value), use `securityPostDenormalize` instead. It runs after the incoming
156+
data has been applied to the object, and provides both `object` (the updated entity) and
157+
`previous_object` (a clone of the entity before the write).
158+
159+
When the `securityPostDenormalize` check fails for a property, the value is **silently reverted** to
160+
its previous value (from `previous_object`) rather than producing an error. The request still
161+
succeeds for any other properties the user is permitted to modify. To instead return a 403 response,
162+
set the `throw_on_access_denied` extra property on the operation or on the individual property
163+
(see the section below).
164+
165+
```php
166+
<?php
167+
// api/src/Entity/Book.php
168+
namespace App\Entity;
169+
170+
use ApiPlatform\Metadata\ApiProperty;
171+
172+
class Book
173+
{
174+
// Combines a static role check (security) with an object-aware ownership check (securityPostDenormalize)
175+
#[ApiProperty(
176+
security: "is_granted('ROLE_EDITOR')",
177+
securityPostDenormalize: "is_granted('FLAVOR_EDIT', object)"
178+
)]
179+
private ?string $flavor = null;
180+
}
181+
```
182+
183+
In this example:
184+
185+
- `security` blocks users without `ROLE_EDITOR` from writing the property entirely (evaluated
186+
before denormalization, `object` is `null`)
187+
- `securityPostDenormalize` further restricts writes to users granted `FLAVOR_EDIT` on the
188+
**updated** object (evaluated after denormalization, `object` is the populated entity)
189+
128190
## Executing Access Control Rules After Denormalization
129191

130192
In some cases, it might be useful to execute a security after the denormalization step. To do so,

0 commit comments

Comments
 (0)