core icon indicating copy to clipboard operation
core copied to clipboard

"object" null in security attribute for canAccessAttribute() on PUT/PATCH

Open weaverryan opened this issue 2 years ago • 8 comments

API Platform version(s) affected: x3.1

Description

Imagine this property on an ApiResource:

class Cake
{
    // ...

    #[ApiProperty(security: 'is_granted("FLAVOR", object)')]
    private ?string $flavor = null;
}

During a GET operation, the object variable is the Cake object. Expected! 🥇 However, during a PATCH operation, the object variable is null during deserialization.

I believe this is a bug, as there IS an object available during deserialization for PUT/PATCH. The current behavior makes it impossible to make flavor READable using the security expression without preventing it from always failing to be WRITEable because the object is null (and your voter probably needs the object).

The workaround is bizarre:

class Cake
{
    // ...

    #[ApiProperty(security: 'object === null or is_granted("FLAVOR", object)')]
    private ?string $flavor = null;
}

You have to allow the security to pass if the object is null so that deserialization works for PUT/PATCH. Then, during serialization, object will be populated so the voter will always run.

How to reproduce
Small reproducer! https://github.com/weaverryan/api_platform_null_object_security_reproducer

Possible Solution
Somewhere around https://github.com/api-platform/core/blob/60075501a6d15d03d4db26f00715a52cac068977/src/Serializer/AbstractItemNormalizer.php#L443, if $object is null, we look also for $context['object_to_populate'].

weaverryan avatar Aug 17 '23 16:08 weaverryan

Aren't you looking for securityPostDenormalize?

mrossard avatar Aug 17 '23 19:08 mrossard

In the security rule you should have access to the existing value in DB of the object, but not to the data that is currently being deserialized (to do so postDenormalizationSecurity must be used, as pointed out by @mrossard). Is the initial value not available?

dunglas avatar Aug 19 '23 05:08 dunglas

The goal is to make the field only readable if the security rule passes. That works well hence using “security”.

The problem is that this has a side effect: the field is now never writable.

I can’t find a way to make the field readable only if the security expression passes without it becoming never writable. I actually don’t want any “write” security on this field (it’s protected on the operation level). I had assumed that, even though I don’t care, the security expression would work the same during read and PATCH.

another solution is have a way to have read security that is different than write security.

weaverryan avatar Aug 19 '23 14:08 weaverryan

The goal is to make the field only readable if the security rule passes. That works well hence using “security”.

The problem is that this has a side effect: the field is now never writable.

I can’t find a way to make the field readable only if the security expression passes without it becoming never writable. I actually don’t want any “write” security on this field (it’s protected on the operation level). I had assumed that, even though I don’t care, the security expression would work the same during read and PATCH.

another solution is have a way to have read security that is different than write security.

I think i remember having a similar problem, trying to find the right combination of security / securityPostDenormalize to get it working as I wanted. If i remember well i ended up giving up since it became overly complicated and it wasn"t a big deal in that app... You can still achieve a similar effect with a custom provider by not setting the field if your condition is not fulfilled, can't you?

mrossard avatar Aug 19 '23 14:08 mrossard

You can still achieve a similar effect with a custom provider by not setting the field if your condition is not fulfilled, can't you?

Hmm, yea,. I think that would work - good point!

Still, it feels that the fix in #5756 feels legit. Kévin mentioned:

In the security rule you should have access to the existing value in DB of the object, but not to the data that is currently being deserialized

But in this case of a PATCH, there IS an existing value in the database, and the data provider has already fetched it. Now, it's denormalizing the JSON data onto that existing object. So it seems consistent that object would be set to OBJECT_TO_POPULATE (i.e. this existing value from the database / from the state provider).

weaverryan avatar Aug 21 '23 14:08 weaverryan

We ran into this ourselves. We ended up having to use "previous_data" instead of object. We were lucky that we were doing it as an entire class security expression on the operation itself.

I fully agree that object should be populated for a PATCH as there is an existing object available from a provider.

nesl247 avatar Sep 19 '23 11:09 nesl247

can anyone provide a patch or a test case?

soyuka avatar Oct 17 '23 10:10 soyuka

I do have a patch on #5756 - but not a test case - I'd be happy if someone smarter than me could find a way to add one

weaverryan avatar Oct 17 '23 14:10 weaverryan