-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Be able to know the reasons of the denied access #35592
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
Conversation
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 like this PR very much 😄 I hope it's gonna be appreciated by the core team!
In the meantime I have a few comments :)
src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/AccessTrait.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
Show resolved
Hide resolved
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.
we should re-evaluate #21094 (comment) first
AccessDecision
vs Vote
is confusing, there should be only 1 IMHO. AFAIK there's no real reason to merge multiple votes now, which is a new behavior
src/Symfony/Component/Security/Core/Authorization/Voter/ExplainedVoterInterface.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
Outdated
Show resolved
Hide resolved
/** | ||
* Sets an access decision and appends the denied reasons to the exception message. | ||
*/ | ||
public function setAccessDecision(AccessDecision $accessDecision) |
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.
this cannot be stateful IMHO
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.
What do you suggest ?
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.
We could make it stateless and have a specific exception render for AccessDeniedException. WDYT ?
We need a distinction between 1 vote amongst others and the final decision. E.g. 3 votes in + 4 votes against = decision to deny (in a consensus strategy). So, a voter votes (it returns a |
Not sure it would bring much: |
src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig
Show resolved
Hide resolved
{% endif %} | ||
</td> | ||
<td class="font-normal text-small">{{ voter_detail['vote'].reason }}</td> |
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.
maybe only display if not empty reason?
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.
The td
html node must always be present.
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 believe we are used to display ~
or (unknown)
to not let it blank. Could you check and apply what looks better?
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.
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.
@javiereguiluz would you have a suggestion here maybe ?
src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php
Outdated
Show resolved
Hide resolved
2261746
to
69e8d07
Compare
This comment has been minimized.
This comment has been minimized.
src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/AccessTrait.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/AccessDecision.php
Outdated
Show resolved
Hide resolved
src/Symfony/Component/Security/Core/Authorization/Voter/Vote.php
Outdated
Show resolved
Hide resolved
trait AccessTrait | ||
{ | ||
/** @var int */ | ||
protected $access; |
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.
Should be private.
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.
Already discussed here #35592 (comment). Ok for you ?
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 sure to understand the rationale there.
If a class uses this trait, it does have write access to the private properties defined by the trait. Using protected
would only serve for inheritance purpose, which is not needed here AFAIK.
Am I missing something?
Co-Authored-By: Antoine Makdessi <antoine.makdessi@agriconomie.com>
Co-Authored-By: Antoine Makdessi <antoine.makdessi@agriconomie.com>
Co-Authored-By: Antoine Makdessi <antoine.makdessi@agriconomie.com>
Changes committed from your feedbacks, thanks ! This PR looks appreciated and its design seems validated, so I'm gonna complete it by writting the tests soon. |
@@ -11,13 +11,22 @@ | |||
|
|||
namespace Symfony\Component\Security\Core\Authorization\Voter; | |||
|
|||
/** | |||
* A Vote is returned by a Voter and contains the access (granted, abstain or denied). It can also contains a reason |
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.
* A Vote is returned by a Voter and contains the access (granted, abstain or denied). It can also contains a reason | |
* A Vote is returned by a Voter and contains the access (granted, abstain or denied). | |
* It can also contains a reason explaining the vote decision. |
Thanks for create this PR! What about creating multiple reasons to deny access, like you can with workflows? |
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 like this idea. I think I also agree on the difference between Vote
and AccessDecision
. An access decision is a collection of votes.
As commented, there is a need for BC layers in the usage part. I guess the only possible method to change this would be to create a new method/new interface. In that case, I think it makes sense to also think a little bit about how we're going to solve #28868 (comment) in the future, as it might also require a different authorization API. If we need to create a new authorization API for this change, I think it's best to take that change into account as well (not fix it in this PR, but designing an API that allows it to happen without change). On second thought, let's not combine these things and only focus on this feature. The other issue can be implemented using the current interface as well.
@@ -33,7 +33,7 @@ interface VoterInterface | |||
* @param mixed $subject The subject to secure | |||
* @param array $attributes An array of attributes associated with the method being invoked | |||
* | |||
* @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED | |||
* @return int|Vote either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED |
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.
The same thing applies here: I don't think we can change the method return signature from int
to Vote
, as it'll impact how you need to use this method.
@@ -26,7 +26,7 @@ interface AccessDecisionManagerInterface | |||
* @param array $attributes An array of attributes associated with the method being invoked | |||
* @param object $object The object to secure | |||
* | |||
* @return bool true if the access is granted, false otherwise | |||
* @return bool|AccessDecision Returning a boolean is deprecated since Symfony 5.1. Return an AccessDecision object instead. |
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.
This interface is not internal, so we should also be BC with the usage of the method AFAIK. I don't think we currently preserve backwards compatibility in e.g. this example:
if (!$accessDecisionManager->decide(...)) {
throw new AccessDeniedException();
}
If an AccessDecision::createDenied()
is returned, the condition !(AccessDecision::createDenied())
is false
, causing the access denied exception not to be throw (while it was thrown using the old bool
signature).
$this->lastAccessDecision = $this->accessDecisionManager->decide($token, [$attribute], $subject); | ||
|
||
if (\is_bool($this->lastAccessDecision)) { | ||
trigger_deprecation('symfony/security', 5.1, 'Returning a boolean from the "%s::decide()" method is deprecated. Return an "%s" object instead', \get_class($this->accessDecisionManager), AccessDecision::class); |
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.
trigger_deprecation('symfony/security', 5.1, 'Returning a boolean from the "%s::decide()" method is deprecated. Return an "%s" object instead', \get_class($this->accessDecisionManager), AccessDecision::class); | |
trigger_deprecation('symfony/security-core', 5.1, 'Returning a boolean from the "%s::decide()" method is deprecated, return an "%s" object instead', \get_class($this->accessDecisionManager), AccessDecision::class); |
private function vote(VoterInterface $voter, TokenInterface $token, $subject, array $attributes): Vote | ||
{ | ||
if (\is_int($vote = $voter->vote($token, $subject, $attributes))) { | ||
trigger_deprecation('symfony/security', 5.1, 'Returning an int from the "%s::vote()" method is deprecated. Return a "%s" object instead.', \get_class($this->voter), Vote::class); |
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.
trigger_deprecation('symfony/security', 5.1, 'Returning an int from the "%s::vote()" method is deprecated. Return a "%s" object instead.', \get_class($this->voter), Vote::class); | |
trigger_deprecation('symfony/security-core', 5.1, 'Returning an int from the "%s::vote()" method is deprecated, return a "%s" object instead.', \get_class($this->voter), Vote::class); |
return new self(VoterInterface::ACCESS_DENIED, $reason, $parameters); | ||
} | ||
|
||
public function merge(self $vote): void |
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 don't really see the value of this method. It doesn't fullfill what I would call "merge" (i.e. it only merges the reason, what happens if we pass denied into the merge of a granted vote?) and it's used only in one specific case. I would rather add a setReason()
method and doing this directly in the voter.
I think this comment from my review is the most important: How can we make this change BC? |
Will we be able to get in in next release before feature freeze? |
@deguif Depends on when it's ready to be merged. We still have 2 weeks for 5.2. |
Cant we use the same logic as the https://github.com/symfony/symfony/blob/master/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php? a proxy class that handles previous/new voting feature? (I am unsure if it can be applied here) concerning the feature itself, i think it is usefull :) (like the workflow transition blocker) (i can help test it if needed :)) |
This seems like very close to what was done for Workflow, guards got the same ability there from setting a boolean value, likely the same approach can be taken. |
@maidmaid Still interested in working on this? This can still be part of 5.2 if approved before tomorrow. |
Sorry for the delay. I missed the 5.2 train I guess. I'm gonna resume this work soon. Let's wait 5.3 :) |
We've just moved away from |
@maidmaid friendly ping, |
@noniagriconomie I don't have a lot of time to finish this work. I would be happy if someone finished it :) |
Hey @maidmaid, i would like to pick up where you left here and continue working on this PR, is that okay with you? |
@azjezz i would be pleased to help you, review or testing in real project if you need :) |
@ noniagriconomie see #40711 |
…e (nicolas-grekas) This PR was merged into the 7.3 branch. Discussion ---------- [Security] Add ability for voters to explain their vote | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | no | Deprecations? | no | Issues | Fix #27995, #26343, #35592, #43147 | License | MIT This PR takes over #58107, which itself took over from #46493, etc. It takes the only approach I could think of that would preserve BC. The visible tip of what this achieves is:   Internally, this provides a new access audit log infrastructure that relies on new `AccessDecision` and `Vote` objects. Note that I didn't add (nor fix) tests for now. I'll borrow (and credit) from #58107 / #46493 as much as possible. Commits ------- c6eb9ae [Security] Add ability for voters to explain their vote
About the voters, the gif of the the original issue sums up the situation well :
Currently, the voters can only say yes or no. This PR allows to give a reason from the voters.
Before :
After :
A voter returns no more an int based on the constant
ACCESS_{GRANTED,ABSTAIN,DENIED}
but aVote
object which contains the access result and the reason. Thus, the security panel of the profiler can display the reason of each vote.The access decision manager returns no more a bool for the final verdict but an
AccessDecision
object which contains the final verdict and all the votes. Thus, the 403 response can display all the denied raisons.It's in a PoC state. Todo : BC layer, migration of the others voters, update current tests, add new tests...