Skip to content

Clarify VoterInterface with multiple attributes #32956

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

Closed
wants to merge 1 commit into from

Conversation

BenMorel
Copy link
Contributor

@BenMorel BenMorel commented Aug 5, 2019

Q A
Branch? 3.4
Bug fix? no
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets none
License MIT
Doc PR none

According to the Voter source code, passing multiple attributes to vote() acts as an OR, i.e. grant access if any of the attributes is granted.

It looks like this is not documented, neither in the source code nor in the docs.

@nicolas-grekas nicolas-grekas added this to the 3.4 milestone Aug 5, 2019
@nicolas-grekas
Copy link
Member

nicolas-grekas commented Aug 5, 2019

Please don't remove the table in the PR template, it's mandatory. Please don't remove any lines either in it.

@xabbuh xabbuh added the Security label Aug 5, 2019
@BenMorel
Copy link
Contributor Author

BenMorel commented Aug 5, 2019

@nicolas-grekas Sorry, it felt useless for a docblock update. Fixed.

@@ -30,6 +30,9 @@ interface VoterInterface
* This method must return one of the following constants:
* ACCESS_GRANTED, ACCESS_DENIED, or ACCESS_ABSTAIN.
*
* If more than one attribute is passed, the voter must grant access if access is granted to any of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this is not a hard requirement for any voter and it's totally fine to implement a custom voter that behaves differently. I would suggest that we move this comment into the docblock of the Voter class instead.

Copy link
Contributor Author

@BenMorel BenMorel Aug 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be very careful with this approach, as it might bring a lot of confusion.

There clearly are conflicting views here. Here's how the Twig doc for is_granted() evolved in the last few months:

  • on 9 Oct 2018, Improve is_granted documentation symfony-docs#10461 by @Jean85 changed the wording to:

    Returns true if the current user has the required role, if only one is passed; if more than one is passed, with an array, the behavior depends on how the Access Decision Manager is configured; by default, the user has to have at least one of the passed roles.

  • changed the same day by @javiereguiluz to:

    Returns true if the current user has the given role. If several roles are
    passed in an array, returns true if the user has all of them or at least one
    of them, depending on the value of this option:
    security.access_decision_manager.strategy.

  • on 25 Oct 2018, following this comment on #10461 by @xabbuh:

    The access decision strategy defines how the result looks like when multiple voters voted on a certain attribute. It does not affect the internal behaviour of the RoleVoter.

    fix RoleVoter behaviour description symfony-docs#10580 by @xabbuh changed it to:

    Returns true if the current user has the given role. If several roles are passed in an array, true is returned if the user has at least one of them.

  • on 18 July 2019, [Security] Better explain what happens when multiple roles are passed symfony-docs#11988 by @javiereguiluz changed it back to:

    Returns true if the current user has the given role. If several roles are passed in an array, true is returned if the user has at least one of them or all of them, depending on the Access Decision Manager strategy.

As you can see, on the assumption that Twig's is_granted() follows the same rules as VoterInterface::vote(), there is no current consensus on how is_granted() should behave, and worse, there are regressions in the docs, ignoring relevant past comments:

  • @Jean85 and @javiereguiluz think that the behaviour is influenced by the Access Decision Manager strategy, although @xabbuh proved that it's not; @Jean85 admitted that this was a mistake.

  • regardless of the point above, @xabbuh now thinks that it's fine for voters to behave as they wish on this point, upvoted by @ro0NL

  • the current Twig doc is clearly in infringement with the source code anyway, as it says that:

    true is returned if the user has at least one of them or all of them, depending on the Access Decision Manager strategy

    which directly contradicts @xabbuh's argument:

    the access decision strategy defines how the result looks like when multiple voters voted on a certain attribute (...) not the internal behaviour of the RoleVoter.

  • and the Voter class is opinionated about what should happen:

    grant access as soon as at least one attribute returns a positive response

IMHO it was an unfortunate design decision to allow multiple values in the first place (forcing isGranted('X') || isGranted('Y') or isGranted('X') && isGranted('Y') would have probably been much less confusing), now one will always wonder what the behaviour is should they pass multiple values, and going to the docs or the source code will only make things more confusing, until they're all in sync on this point.

Now, to make things right without breaking BC, I would recommend to apply the "less bad" fix:

  • choose one, and only one, appropriate, documented behaviour here (most likely the at least one of them strategy)
  • update all docs (Symfony Security component, Twig, source code) to reflect this fact
  • maybe find a way to ensure that another PR a few months down the road won't change this again (how?)

@jvasseur
Copy link
Contributor

jvasseur commented Aug 6, 2019

I think we should deprecate passing multiple attributes instead :

@BenMorel
Copy link
Contributor Author

BenMorel commented Aug 6, 2019

@jvasseur

The behavior is completely unspecified and every voter can chose its own strategy
Except when using the unanimous srategy where it is force to an AND

I guess you also mix the Voter and the Access Decision Manager: even if the strategy is unanimous, you can still, right now, have a Voter that will grant access if any attribute is granted; this is the default behaviour of the Voter class, and is unaffected by the decision manager strategy.

It's always more clear to explicitly specify if you want an AND or an OR when calling is_granted instead of relying of an behavior that you have to remember

Agreed. I also think that multiple arguments should be deprecated. This is a discussion for another PR though, right now I think a concensus has to be found, and the documentation clearly updated to reflect this.

@jvasseur
Copy link
Contributor

jvasseur commented Aug 6, 2019

The behavior is completely unspecified and every voter can chose its own strategy
Except when using the unanimous srategy where it is force to an AND

I guess you also mix the Voter and the Access Decision Manager: even if the strategy is unanimous, you can still, right now, have a Voter that will grant access if any attribute is granted; this is the default behaviour of the Voter class, and is unaffected by the decision manager strategy.

You can have a Voter that return access granted if any attribute is granted.

But when using the unanimous strategy the access decision manager will do one call to the voter per attribute passed with only this attribute in the attribute array and then do an AND with the results, thus ignoring the voter behavior :

foreach ($this->voters as $voter) {
foreach ($attributes as $attribute) {
$result = $voter->vote($token, $object, [$attribute]);
switch ($result) {
case VoterInterface::ACCESS_GRANTED:
++$grant;
break;
case VoterInterface::ACCESS_DENIED:
return false;
default:
break;
}
}
}

@BenMorel
Copy link
Contributor Author

BenMorel commented Aug 6, 2019

But when using the unanimous strategy the access decision manager will do one call to the voter per attribute passed with only this attribute in the attribute array and then do and AND with the results, thus ignoring the voter behavior

Indeed, thanks for pointing this out. The fact is, this is all very confusing to newcomers and regulars alike, the documentation really needs to be improved and be consistent on this point.

@javiereguiluz
Copy link
Member

javiereguiluz commented Aug 6, 2019

I agree that this is super confusing ... and I'd vote to deprecate/remove this feature. If you want to check multiple roles (something I've never used personally and something I've never seen in any Symfony app), then use the PHP constructs isGranted() && &isGranted() or isGranted() || isGranted() as @BenMorel said.

@jvasseur
Copy link
Contributor

This can be closed now that #33584 is merged.

@stof stof closed this Sep 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants