-
-
Notifications
You must be signed in to change notification settings - Fork 900
feat: add security_post_denormalize
in ApiProperty
#4184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add security_post_denormalize
in ApiProperty
#4184
Conversation
c9643ea
to
5436060
Compare
Related: #3966 @DavidBennettUK I think @Toflar is able to distinguish between an object and a class string by using a custom expression language function ( |
|
||
// Filter out input attributes that aren't allowed for updates | ||
if ($isUpdate) { | ||
$data = array_filter($data, function (string $attribute) use ($context, $objectToPopulate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why filtering the input? Was it possible to modify a property of a resource without the right to do so before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right that there was already a security pass - AbstractObjectNormalizer::denormalize()
calls AbstractItemNormalizer::getAllowedAttributes()
but only with the class string, so object
can't be used. This addition does another pass over, calling the security
expression with a populated object
variable. Without this, security
will always be called with a null object for updates, which may be unexpected. We could take this out and force use of security_post_denormalize
if the developer wants an object, but I think an object would be expected for updates as it already exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be in favor of reducing the complexity in our code, enforcing the use of security_post_denormalize
In my case, I cannot check on the roles of the user directly because the same user has different roles for different organisations. So what I did is this: /**
* @ApiProperty(security="user_has_role_for_organisation('ROLE_OWNER', object)")
*/
private BillingInfo $billingInfo; And then a simple class AccessExpressionProvider implements ExpressionFunctionProviderInterface
{
public function getFunctions(): array
{
return [
new ExpressionFunction('user_has_role_for_organisation', function (string $role, Organisation $organisation) {
return sprintf('$user->hasRoleForOrganisation(%s, %s)', $role, $organisation);
}, function (array $variables, string $role, Organisation $organisation) {
return $variables['user']->hasRoleForOrganisation($role, $organisation);
}),
];
}
} This should still work as far as I can see. |
That's weird. If I understand what @DavidBennettUK is saying, your function should fail when doing an update / create, because |
Everywhere I use this, it's only |
It makes sense then! It should fail if you try an update. |
Might very well be but as this is not a requirement for me, I have no tests for that :) |
No problem 🙂 |
|
||
// Filter out input attributes that aren't allowed for updates | ||
if ($isUpdate) { | ||
$data = array_filter($data, function (string $attribute) use ($context, $objectToPopulate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be in favor of reducing the complexity in our code, enforcing the use of security_post_denormalize
@soyuka thanks for the feedback, I've made the suggested changes 👍 |
$object = parent::denormalize($data, $resourceClass, $format, $context); | ||
|
||
// Revert attributes that aren't allowed to be changed after a post-denormalize check | ||
foreach (array_keys($data) as $attribute) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was it possible to modify a property of a resource without the right to do so before?
If so, isn't it a bugfix for 2.6?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I'm aware of. This is only reverting the properties that fail the new security_post_denormalize
check. Properties that fail the security
check are still filtered from denormalization via AbstractItemNormalizer::getAllowedAttributes()
* | ||
* @ODM\Field | ||
* @ApiProperty( | ||
* security="object == null or object.getOwner() == user", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it still useful?
* | ||
* @ORM\Column | ||
* @ApiProperty( | ||
* security="object == null or object.getOwner() == user", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it still useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the object
isn't available in creates/updates, in this scenario you'd need to pass the security
check if the object
is null
, as otherwise it would fail this check (object.getOwner()
would throw a null pointer exception) and never run security_post_denormalize
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if there is already a securityPostDenormalize
, why a security
is needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found the answer myself: it's because it is used for the get
operation too.
LGTM. Don't forget the documentation entry. |
Documentation PR api-platform/docs#1349 |
f4a0e41
to
4e5ded5
Compare
…ct` passed to the expression during denormalization when trying to update or create a resource. This meant that only simple role checks etc. could be used. Now the object can be used to grant access directly, passed to voters, etc. If the object isn't available, then null is passed as the `object` instead, making it easier to check for this condition in the expression. Also implements a `security_post_denormalization` attribute on `ApiProperty` to perform another check after denormalisation (for create/update) and be guaranteed that there is an `object` (as well as `previous_object` for updates).
…tadata instead of reflection directly.
… reduce code complexity. `security_post_normalize` should be used if an object is required.
4e5ded5
to
c178a85
Compare
ApiProperty
security
expressions not supporting objects in updates/createssecurity_post_denormalize
in ApiProperty
Thank you very much @DavidBennettUK! |
While implementing #4143 I noticed that it's not possible to use
object
in the security expression ofApiProperty
- not only for GraphQL, but for all.It was already possible to use
object
for read operations - but doing so would cause unexpected exceptions to be thrown on updates/creates, as theobject
is actually a class string. For example, the security expressionobject.getOwner() == user
would throw an exception when performing an update asobject
would be a class string - which the developer wouldn't expect. This exception would happen even if you weren't updating that specific field, as the SymfonyAbstractObjectNormalizer::denormalize()
callsgetAllowedAttributes
- which runs security on every single field, due tocanAccessAttribute
being overridden inAbstractItemNormalizer
.There didn't seem to be a way to work around this in the expressions, as there isn't an obvious way to check if a variable is an object or string in the expression language.
This PR updates this so that
object
will always be the object or null - which can easily be checked for in the expression.Also added is a
security_post_denormalize
which works similarly to those onApiResource
. This lets you be guaranteed thatobject
isn't null and will also give you access toprevious_object
for more advanced checks.This should hopefully not be a BC break unless developer workarounds were put in place possibly. For safety, and because it depends on #4143, it is targeting main.