-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Fix the supports() method argument type of the security voter #37325
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
Fix the supports() method argument type of the security voter #37325
Conversation
Should this doc be changed to |
As I said, if we force typing on a Only the helper |
This change should be covered by a test. Also, your change might break some (probably rare) scenarios that would work right now:
|
Of course, I'm going to add a test :-)
Indeed, we can very well have a stringable object, however, as has never been indicated in the documentation or the phpdoc, some voters are therefore not stringable.
It was my first intention, however, we will break the Backward Compatibility if some voters override the |
All voters that extend the abstract
I'm not suggesting to change the |
Not all tests are passed in php 7.3, but these are tests for the Cache component. There was no error in the previous commit of this PR, so maybe just restart the tests for php 7.3 will remove the error |
@derrabus We do indeed agree on the 2 points. However, send a non-stringable attribute to a voter extending the abstract class `Voter', and you will get the error:
Given that the |
I know, This is why I said v6. |
Not necessarily, please see the second 3v4l link I posted. |
Why limit arbitrarily the type to an array of strings? It is only the aid class to build the Voter that has become blocking. In addition, if we just change the phpdoc to The principle of the |
Remove my modification in the class |
Again, please see the second 3v4l link I posted above. It shows you a voter that passes. |
Sorry, but I will repeat myself :-), but the test passes with my modification, but without my modification you get:
|
All right, I give up. 🤷🏻♂️ |
@francoispluchino try a testcase with a custom voter that removes the string type here; symfony/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php Line 69 in 73ce604
we could simply remove the string types in core:
|
Yes, I had misunderstood the meaning of your answer, I am in the process of making the modification for this case. |
@ro0NL If we delete the type PHP Fatal error: Declaration of Symfony\Component\Security\Core\Tests\Authorization\Voter\VoterTest_Voter::supports(string $attribute, $object): bool m
ust be compatible with Symfony\Component\Security\Core\Authorization\Voter\Voter::supports($attribute, $subject) in E:\Git\php\symfony\symfony\src\Symfo
ny\Component\Security\Core\Tests\Authorization\Voter\VoterTest.php on line 72 |
Ah true. We could reflect e.g. |
I just added the test concerning the use case of attributes with a type |
As I point out to @ro0NL, your example can break compatibility with Voters implementing the new signature (see this comment). However, thank you for your example concerning the fact that a stringable object can no longer be validated by a Voter when it was the case before my PR. I just add this use case. |
There's still the second case: https://3v4l.org/MrcDq, where attributes are just objects (non stringable), or maybe someone uses ints. |
@ro0NL Yes, but in this case, the Voter cannot extend the abstract class |
Im not sure i follow now 😕 https://3v4l.org/MrcDq bool(true) |
I don't think it is advisable to remove the typing from the method |
You remove the type of the Note: However, your example is still valid, but if we remove the type in the abstract class, an error is thrown if the Voter use the new signature. It is for this reason that I chose to do a test before calling the method |
We cannot do this and nobody suggested to do this. Yet, the type can be removed by userland code when implementing the voter. That is a valid thing to do in php and that is all that my second example shows. And since the type declaration wasn't there in Symfony 2/3/4, there are probably quite a few voter implementations out there that do not have the type declaration. Expecting an object (or anything but a string) at this point might not be an execution path that was intended, but it's one that currently works and that will be broken by the change you're suggesting. But I don't really know how to deal with it. It might even be okay to break it. |
Yes, I was looking for a solution when we delete the type. However, I don't see how to check whether the typing is present or not on the instantiated class. |
The problem arises when we want to call the method |
You you catch and parse the |
It's not a problem to add the méthode
Totally agree with you, but I don't see any other more elegant solution. |
I just modified the PR to use a try/catch for the TypeError, and added tests for the attributes of type |
Apart from |
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
Outdated
Show resolved
Hide resolved
@wouterj how would you implement the I'm all in favor of restricting what we can use as attributes but I'm not sure it's a reasonable move. Another thing to take into account is that PHP will probably add support for enums at some points and people will want to use enums as attributes. |
Let's not take PHP enums into consideration. First of all, it'll take a while before it will be added (it's already discussed for many years). Secondly, as everyone is now migrating to strict typed code bases, they will need to provide compatibility with current day strict typing for enums (e.g. an enum of strings should be able to be passed to a About |
That's a change we can hardly anticipate right now, can we? There are however libraries like |
@wouterj. I aggree with you, but for the moment, it is necessary to fix the bug, and depreciate or not for version 5.2 and prohibit the use of a non-string attribute in version 6.0, if we decide to take this direction. Personally I have a use case which led me to do it like voting Example 1: In my case, I use several voters including one which allows to validate permissions according to the subject. This subject can very well be a string (name of the class), an instance of a class, or an instance of a In attribute, I used a permission name in the same style as the // Check the authorization on the object
$authorizationChecker->isGranted('perm_update', $entity);
$authorizationChecker->isGranted('perm_update', PostInterface::class);
// Check the authorization on the field of object
$authorizationChecker->isGranted('perm_edit', new FieldVote($entity, 'title'));
$authorizationChecker->isGranted('perm_edit', new FieldVote(PostInterface::class, 'title')); As you can see, it is not "natural" for a user to have to do magic by prefixing the name of the permission with: Of course, I could use an array of the style Using the example below is much more meaningful and logical, while being certain of the expected behavior: // Check the authorization on the object
$authorizationChecker->isGranted(new PermissionVote('update'), $entity);
$authorizationChecker->isGranted(new PermissionVote('update'), PostInterface::class);
// Check the authorization on the field of object
$authorizationChecker->isGranted(new PermissionVote('edit'), new FieldVote($entity, 'title'));
$authorizationChecker->isGranted(new PermissionVote('edit'), new FieldVote(PostInterface::class, 'title')); You can see the old version of the component using the prefix The fact of define the permission in the attribute and leaving the subject to the Of course, it is possible to make the class Example 2: There is also another reason to use something other than an attribute string. This time, it's with a component validating Oauth2 scopes. The Voter waits in attribute for an instance of the class The class You will therefore understand that there is still a possibility of unexpected behavior if we put in attribute, only the name of the scope without prefix like // Check if the current user is authorized to read a Post with a his Oauth2 scopes
$authorizationChecker->isGranted(new ScopeVote(['post', 'post.readonly'], false));
// Check if the current user is authorized to manage a Post with a his Oauth2 scopes
$authorizationChecker->isGranted(new ScopeVote('post'));
Another problem if we use a string instead of an instance of an object, it is likely easier that another Voter can validate the attribute and the subject when it was not intended for him. With an object, we can be certain that we reach the correct Voter which checks if the instance of the attribute is compatible, while making the code more readable and understandable. And as I said, put a For example, it is more complicated to have: $authorizationChecker->isGranted('permission', new PermissionVote('edit', new FieldVote(PostInterface::class, 'title')));
$authorizationChecker->isGranted('scope', new ScopeVote('post')); that: $authorizationChecker->isGranted(new PermissionVote('edit'), new FieldVote(PostInterface::class, 'title'));
$authorizationChecker->isGranted(new ScopeVote('post')); Regarding strict typing, I agree with you, and it is also for this reason that I used classes as an attribute, knowing that the documentation of Symfony authorized it. Of course, we can very well decide to deprecate the use of non-string attribute, but I think it would be more appropriate to allow at least stringable objects. This comment is very long, and I hope I have been understandable enough with my English and the wish to be as brief as possible, but it is ultimately quite difficult :-). |
Yes, agreed. But I think the bug can be fixed a bit more "hacky" if we know we can clean it up in 6.0 :) Thanks for your interesting insights! I need some time to process all the information - I'll follow-up with an RFC issue. Let's finish this bug report - as I agree that a bug has been introduced. |
continue; | ||
} | ||
} catch (\TypeError $e) { | ||
if (preg_match((sprintf('/Argument 1 passed to ([\w\\\]+)::supports\(\) must be of the type string, (\w+) given/')), $e->getMessage())) { |
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.
if (preg_match((sprintf('/Argument 1 passed to ([\w\\\]+)::supports\(\) must be of the type string, (\w+) given/')), $e->getMessage())) { | |
if (0 === strpos($e->getMessage(), 'Argument 1 passed to') && false !== strpos($e->getMessage(), '::supports() must be of the type string')) { |
Strpos seems to be about 50% quicker: https://3v4l.org/47iXF
This method is potentially called quite a lot (for every voter in access_control and all isGranted calls), so it's performance critical, so I think it's worth considering.
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.
Indeed, this is to be considered. Your test is less strict but it is probably sufficient, but above all faster. I make the modification.
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.
It is done.
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.
@wouterj Please keep in mind that the error message format changes with php 8. The error message does not begin with Argument
anymore, but with the method name, IIRC.
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.
Travis agrees:
TypeError: Symfony\Component\Security\Core\Tests\Authorization\Voter\VoterTest_Voter::supports(): Argument #1 ($attribute) must be of type string, stdClass given, called in /home/travis/build/symfony/symfony/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php on line 34
So it probably should do something like:
if (\PHP_VERSION_ID < 80000) {
if (0 === strpos($e->getMessage(), 'Argument 1 passed to') && false !== strpos($e->getMessage(), '::supports() must be of the type string')) {
continue;
}
} else if (false !== strpos($e->getMessage(), 'supports(): Argument #1')) {
continue;
}
throw $e;
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.
@wouterj Thank you, I didn't have PHP 8 on hand right now. I take your code.
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 think almost nobody has. Fortunately, the Symfony test suite runs on PHP 8. You can find its results in the "Travis" link below. The relevant failure is https://travis-ci.org/github/symfony/symfony/jobs/699831814#L4276 (you can ignore the other failures, lots of things are work-in-progress for PHP 8/external dependency support).
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.
It is done.
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.
Thanks for your very quick responses! Looks good to me now (looks like all test failures are unrelated)
Thank you @francoispluchino. |
…ter (francoispluchino) This PR was submitted for the master branch but it was merged into the 5.0 branch instead. Discussion ---------- Fix the supports() method argument type of the security voter | Q | A | ------------- | --- | Branch? | 5.0 and 5.1 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | ~ | License | MIT | Doc PR | ~ Since adding types to method arguments in the version 5.0 (and therefore also 5.1), there is a type mismatch on the first argument of the `supports()` method of the abstract class `Symfony\Component\Security\Core\Authorization\Voter\Voter`. Indeed, the `supports()` method had in previous versions (4.x), the phpdoc indicating that the argument `$attribute` must be a `string`, but this one is not compatible with the `isGranted()` method of the interface `Symfony\Component\Security\Core\AuthorizationAuthorizationCheckerInterface` whose the `$attribute` argument is of type `mixed`. The problem arises when you have voters extending the abstract class `Voter` positioned before a vote with an attribute of a type other than `string`. Apart from Voters created by third parties, there is the voter `ExpressionVoter` which waits in attribute, an instance of the class `Symfony\Component\ExpressionLanguage\Expression` (you can see the [doc](https://symfony.com/doc/current/security/expressions.html) for an example). Just add a voter extending the abstract class `Voter` with a higher priority than the voter `ExpressionVoter` to get the error: ``` Argument 1 passed to FooVoter::supports() must be of the type string, object given ``` To avoid removing the type of the `$attribute` argument from the method `Symfony\Component\Security\Core\Authorization\Voter\Voter::supports(string $attribute, $subject)`, which can break the backward compatibility, you just have to test in the `vote()` method if the attribute is not a `string` and continue before calling the `supports()` method. Commits ------- b8192ee Fix the 'supports' method argument type of the security voter
This is very confusing, and still NOT fixed in Symfony 6.0 - the type there is STILL string, but |
Since adding types to method arguments in the version 5.0 (and therefore also 5.1), there is a type mismatch on the first argument of the
supports()
method of the abstract classSymfony\Component\Security\Core\Authorization\Voter\Voter
.Indeed, the
supports()
method had in previous versions (4.x), the phpdoc indicating that the argument$attribute
must be astring
, but this one is not compatible with theisGranted()
method of the interfaceSymfony\Component\Security\Core\AuthorizationAuthorizationCheckerInterface
whose the$attribute
argument is of typemixed
.The problem arises when you have voters extending the abstract class
Voter
positioned before a vote with an attribute of a type other thanstring
.Apart from Voters created by third parties, there is the voter
ExpressionVoter
which waits in attribute, an instance of the classSymfony\Component\ExpressionLanguage\Expression
(you can see the doc for an example). Just add a voter extending the abstract classVoter
with a higher priority than the voterExpressionVoter
to get the error:To avoid removing the type of the
$attribute
argument from the methodSymfony\Component\Security\Core\Authorization\Voter\Voter::supports(string $attribute, $subject)
, which can break the backward compatibility, you just have to test in thevote()
method if the attribute is not astring
and continue before calling thesupports()
method.