Skip to content

[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

Closed
wants to merge 9 commits into from

Conversation

maidmaid
Copy link
Contributor

@maidmaid maidmaid commented Feb 4, 2020

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? yes
Tickets #27995, #26343
License MIT
Doc PR /

About the voters, the gif of the the original issue sums up the situation well :

gif

Currently, the voters can only say yes or no. This PR allows to give a reason from the voters.

Before :

class PostVoter extends Voter
{
    // ...

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        if ($subject->getAuthor() !== $token->getUser()) {
            return false;
        }

        if ($subject->getStatus() !== 'draft') {
            return false;
        }

        return true;
    }
}

After :

class PostVoter extends Voter
{
    // ...

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        if ($subject->getAuthor() !== $token->getUser()) {
            return $this->deny('You are not the author.')
        }

        if ($subject->getStatus() !== 'draft') {
            return $this->deny('The post is no more a draft.')
        }

        return $this->grant();
    }
}

A voter returns no more an int based on the constant ACCESS_{GRANTED,ABSTAIN,DENIED} but a Vote object which contains the access result and the reason. Thus, the security panel of the profiler can display the reason of each vote.

profiler

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.

exception

It's in a PoC state. Todo : BC layer, migration of the others voters, update current tests, add new tests...

Copy link
Contributor

@Pierstoval Pierstoval left a 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 :)

Copy link
Contributor

@ro0NL ro0NL left a 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

/**
* Sets an access decision and appends the denied reasons to the exception message.
*/
public function setAccessDecision(AccessDecision $accessDecision)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you suggest ?

Copy link
Contributor Author

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 ?

@maidmaid
Copy link
Contributor Author

maidmaid commented Feb 5, 2020

AccessDecision vs Vote is confusing, there should be only 1

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 Vote object), whereas the access decision manager decides the final access (it returns an AccessDecision object). Maybe, AccessDecision could be renamed to something like Verdict for clarity ?

@chalasr chalasr added this to the next milestone Feb 5, 2020
@chalasr
Copy link
Member

chalasr commented Feb 5, 2020

AccessDecision could be renamed to something like Verdict for clarity ?

Not sure it would bring much: AccessDecisionManager takes access decisions, verdict is just a synonym, might be more confusing than helpful.

{% endif %}
</td>
<td class="font-normal text-small">{{ voter_detail['vote'].reason }}</td>
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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?

Copy link
Contributor Author

@maidmaid maidmaid Mar 3, 2020

Choose a reason for hiding this comment

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

image
Much better imo ;)

Copy link
Contributor Author

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 ?

@maidmaid maidmaid force-pushed the vote branch 2 times, most recently from 2261746 to 69e8d07 Compare February 18, 2020 10:31
@maidmaid

This comment has been minimized.

trait AccessTrait
{
/** @var int */
protected $access;
Copy link
Member

Choose a reason for hiding this comment

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

Should be private.

Copy link
Contributor Author

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 ?

Copy link
Member

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?

maidmaid and others added 6 commits March 3, 2020 09:42
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>
@maidmaid
Copy link
Contributor Author

maidmaid commented Mar 3, 2020

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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.

@stephanvierkant
Copy link
Contributor

Thanks for create this PR!

What about creating multiple reasons to deny access, like you can with workflows?

Copy link
Member

@wouterj wouterj left a 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
Copy link
Member

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.
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Member

@wouterj wouterj Apr 24, 2020

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.

@fabpot
Copy link
Member

fabpot commented Aug 12, 2020

@wouterj @maidmaid What would be the next steps to move this PR forward?

@wouterj
Copy link
Member

wouterj commented Aug 13, 2020

I think this comment from my review is the most important: How can we make this change BC?

@deguif
Copy link
Contributor

deguif commented Sep 16, 2020

Will we be able to get in in next release before feature freeze?

@fabpot
Copy link
Member

fabpot commented Sep 16, 2020

@deguif Depends on when it's ready to be merged. We still have 2 weeks for 5.2.

@noniagriconomie
Copy link
Contributor

@maidmaid @wouterj @fabpot

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 :))

@dkarlovi
Copy link
Contributor

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.

@fabpot
Copy link
Member

fabpot commented Sep 30, 2020

@maidmaid Still interested in working on this? This can still be part of 5.2 if approved before tomorrow.

@maidmaid
Copy link
Contributor Author

maidmaid commented Oct 2, 2020

Sorry for the delay. I missed the 5.2 train I guess. I'm gonna resume this work soon. Let's wait 5.3 :)

@fabpot fabpot closed this Oct 7, 2020
@fabpot
Copy link
Member

fabpot commented Oct 7, 2020

We've just moved away from master as the main branch to use 5.x instead. Unfortunately, I cannot reopen the PR and change the target branch to 5.x. Can you open a new PR referencing this one to not loose the discussion? Thank you for your understanding and for your help.

@noniagriconomie
Copy link
Contributor

@maidmaid friendly ping,
are you able to complete this code for the next version 5.3? If not, can help be provided?
(related to #27995 (comment) and following comments)

@maidmaid
Copy link
Contributor Author

maidmaid commented Mar 5, 2021

@noniagriconomie I don't have a lot of time to finish this work. I would be happy if someone finished it :)

@azjezz
Copy link
Contributor

azjezz commented Apr 4, 2021

Hey @maidmaid, i would like to pick up where you left here and continue working on this PR, is that okay with you?

@noniagriconomie
Copy link
Contributor

@azjezz i would be pleased to help you, review or testing in real project if you need :)

@azjezz
Copy link
Contributor

azjezz commented Apr 6, 2021

@ noniagriconomie see #40711

alamirault added a commit to alamirault/symfony that referenced this pull request May 26, 2022
fabpot added a commit that referenced this pull request Feb 17, 2025
…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:

![image](https://github.com/user-attachments/assets/67bd0678-868f-40ed-b2c6-3b1f10ffe101)

![image](https://github.com/user-attachments/assets/715814d8-e4de-47f0-aa0e-c412092389ff)

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
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.