Skip to content

[Security] Add strategy resolvers #21178

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

Conversation

fancyweb
Copy link
Contributor

@fancyweb fancyweb commented Jan 6, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #15130 (kind of)
License MIT
Doc PR -

This PR is a way to be able to use a specific strategy (in the AccessDecisionManager) for particular attributes / object / token. Cf #15130 for more discussion about this.
I know the idea in the issue was rejected but this implementation is different and time have passed since then, so maybe there will be a new look on this.

return;
}

$strategyResolvers = new \SplPriorityQueue();
Copy link
Contributor

Choose a reason for hiding this comment

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

As a new feature you're PR should be based on master, you will then be able to use the trait, instead of relying on this as it does not preserve the original order in which the services have been registered (ref #20995).

Copy link
Contributor Author

@fancyweb fancyweb Jan 6, 2017

Choose a reason for hiding this comment

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

Oh sorry I didn't know new feature have to be on master, I only did patch PR until now

I rebased my branch and realized there is still some work that needs to be done because of the TraceableAccessDecisionManager (the fact that the strategy is not unique now).

Unrelated but about your comment on the PriorityTaggedServiceTrait trait : I actually almost copy / pasted the AddSecurityVotersPass when I did mine. I just checked and this one is not using the trait... Should it be done in a separate PR as there might be problems of order too ?

@linaori
Copy link
Contributor

linaori commented Jan 6, 2017

Can you show how this would be used?

@xabbuh xabbuh added this to the 3.x milestone Jan 6, 2017
@fancyweb fancyweb force-pushed the access-decision-strategy-resolver branch from 58e2a60 to 50155a1 Compare January 6, 2017 15:44
@fancyweb fancyweb changed the base branch from 2.7 to master January 6, 2017 15:44
@fancyweb
Copy link
Contributor Author

fancyweb commented Jan 6, 2017

@iltar do you mean a real case example of why someone could need this feature, or a code example of how to use it ?

I succesfully rebased this on master. I will wait a few days for more opinions before continuing working on this. There are still tests to write, and possible improvements in the web profiler.

@@ -47,10 +47,16 @@ public function decide(TokenInterface $token, array $attributes, $object = null)
{
$result = $this->manager->decide($token, $attributes, $object);

$strategy = 'unknown';
if ($this->manager instanceof AccessDecisionManager) {
$strategy = $this->manager->getStrategy($token, $attributes, $object);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like the fact that there is a double call to the getStrategy method (the other in the decide call), but I don't see how else I can get the strategy (unless the last strategy used get stored in the AccessDecisionManager)

@xabbuh
Copy link
Member

xabbuh commented Jan 6, 2017

@fancyweb In #19034, one of the first attempts included the extraction of the access decision strategies into a dedicated interface (called VoteStrategyInterface back then). I then decided to remove them as it will be possible to swap the used implementation of the AccessDecisionManagerInterface with those changes. Would that be enough for you?

@linaori
Copy link
Contributor

linaori commented Jan 6, 2017

@fancyweb yeah, some user-land examples would help illustrate what the feature does

@fancyweb
Copy link
Contributor Author

fancyweb commented Jan 10, 2017

@xabbuh I found your PR when I searched how to change the strategy, but if I understand it correctly, even if strategies are extracted from the AccessDecisionManager, there will still be only one global default strategy used.

After more analysis, I think this feature is only useful when you are using reusable bundles, otherwise you can always make it work the way you want in your voters, since you've got the control on all of them.

@iltar Here is one example that involves a reusable bundle.

YourBundle is a bundle that helps you quickly setup a thread / messages system in your applications. In this bundle, you set up some voters that will be used by the AccessDecisionManager when you need to know if the logged user can perform some actions (like display or not a button in the bundle default templates / secure your controllers actions).

<?php

namespace Company\YourBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Company\YourBundle\Model\ThreadInterface;

class ThreadVoter extends Voter
{
    /**
     * @var string
     */
    const POST_MESSAGE = 'your_bundle_post_message';

    /**
     * @param string $attribute
     * @param mixed $subject
     *
     * @return bool
     */
    protected function supports($attribute, $subject)
    {
        return self::POST_MESSAGE === $attribute && $subject instanceof ThreadInterface;
    }

    /**
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     *
     * @return bool
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        if (null === $token->getUser()) {
            return false;
        }

        /* @var $subject ThreadInterface */
        switch ($attribute) {
            case self::POST_MESSAGE:
                return !$subject->isLocked();
        }

        throw new \LogicException();
    }
}
<?php

namespace Company\YourBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Company\YourBundle\Model\MessageInterface;

class MessageVoter extends Voter
{
    /**
     * @var string
     */
    const DELETE = 'your_bundle_delete_message';

    /**
     * @param string $attribute
     * @param mixed $subject
     *
     * @return bool
     */
    protected function supports($attribute, $subject)
    {
        return self::DELETE === $attribute && $subject instanceof MessageInterface;
    }

    /**
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     *
     * @return bool
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        if (null === $token->getUser()) {
            return false;
        }

        /* @var $subject MessageInterface */
        switch ($attribute) {
            case self::DELETE:
                return $subject->getCreatedBy() === $token->getUser();
        }

        throw new \LogicException();
    }
}

This is the default "common" (rules that will never change) behavior of the bundle :

  • A message can be posted in a thread if the thread has not been locked.
  • A message can be deleted by its author.

Thanks to the voter system, each application using the bundle can add its own rules, on top of the default ones.

<?php

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Company\YourBundle\Model\ThreadInterface;
use Company\YourBundle\Security\ThreadVoter as YourBundleThreadVoter;
use AppBundle\Entity\User;

class ThreadVoter extends Voter
{
    /**
     * @param string $attribute
     * @param mixed $subject
     *
     * @return bool
     */
    protected function supports($attribute, $subject)
    {
        return YourBundleThreadVoter::POST_MESSAGE === $attribute && $subject instanceof ThreadInterface;
    }

    /**
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     *
     * @return bool
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }        

        switch ($attribute) {
            case YourBundleThreadVoter::POST_MESSAGE:
                return $user->isEmailVerified();
        }

        throw new \LogicException();
    }
}
<?php

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Company\YourBundle\Model\MessageInterface;
use Company\YourBundle\Security\MessageVoter as YourBundleMessageVoter;
use AppBundle\Entity\User;

class MessageVoter extends Voter
{
    /**
     * @param string $attribute
     * @param mixed $subject
     *
     * @return bool
     */
    protected function supports($attribute, $subject)
    {
        return YourBundleMessageVoter::DELETE === $attribute && $subject instanceof MessageInterface;
    }

    /**
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     *
     * @return bool
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        if (!$user instanceof User) {
            return false;
        }        

        switch ($attribute) {
            case YourBundleMessageVoter::DELETE:
                return in_array('ROLE_ADMIN', $user->getRoles());
        }

        throw new \LogicException();
    }
}

This is the specific behavior you want in your application :

  • A message can be posted in a thread if the thread has not been locked, and the current logged user email has been verified (you need unanimous strategy).
  • A message can be deleted by its author or by an admin (you need affirmative strategy).

As you can see having a global strategy limits the possibility of defining specific behaviors in your application. I would understand if a majority find that these kind of cases are too uncommon to make a feature for it.

With this feature, you would choose the most common strategy in your application as the default one, and everytime you need another one, you would have to make a StrategyResolver. In our example let's say we keep the affirmative strategy as the default. We would do this :

<?php

namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authorizatin\StrategyResolverInterface;
use Company\YourBundle\Security\ThreadVoter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;

class ThreadStrategyResolver implements StrategyResolverInterface
{
    /**
     * @param TokenInterface $token
     * @param array $attributes
     * @param mixed $object
     *
     * @return string
     */
    public function getStrategy(TokenInterface $token, array $attributes, $object = null)
    {
        // as we only have one case "supported" I directly return the strategy but basically
        // you can do what you want here.
        return AccessDecisionManager::STRATEGY_UNANIMOUS;
    }

    /**
     * @param TokenInterface $token
     * @param array $attributes
     * @param mixed $object
     *
     * @return bool
     */
    public function supports(TokenInterface $token, array $attributes, $object = null)
    {
        return in_array(ThreadVoter::POST_MESSAGE, $attributes) && $object instanceof ThreadInterface;
    }
}
services:
    app.strategy_resolver.thread:
        class: AppBundle\Security\ThreadStrategyResolver
        tags:
            - { name: security.strategy_resolver }

@linaori
Copy link
Contributor

linaori commented Jan 10, 2017

What about a single attribute per voter?

  • CAN_POST_THREAD, vote on whether or not you may (in 1 voter)
  • CAN_DELETE_THREAD, vote on whether or not you may (in 1 voter)

I think you should keep this kind of domain logic in 1 place, especially not in re-usable bundles.

@fancyweb
Copy link
Contributor Author

I'm sorry I don't understand, where this one voter would be ?
Keeping a default behaviour in the reusable bundle reduces code duplication as I don't have to define the same voters in every application. Also without a default voter in the reusable bundle, I wouldn't be able to use is_granted() in the default templates / controllers, so every application would have to override everything.
Isn't it one of the goal of the voter system, to be able to define as many voters we want for the same attributes / subject / token ?

@linaori
Copy link
Contributor

linaori commented Jan 10, 2017

Keeping a default behaviour in the reusable bundle reduces code duplication as I don't have to define the same voters in every application. Also without a default voter in the reusable bundle, I wouldn't be able to use is_granted() in the default templates / controllers, so every application would have to override everything.

I think you might be miss-using bundles: https://stovepipe.systems/post/what-are-bundles-in-symfony

Long story short, bundles should provide infrastructure, not domain logic. Voters are the definition of domain logic.

Isn't it one of the goal of the voter system, to be able to define as many voters we want for the same attributes / subject / token ?

The idea here is that you can have multiple voters for one attribute, but that your application decides how the voting goes (no exceptions).

I'm not for or against the feature, I'm merely wondering if the requirements of this feature is justified or if there's another solution.

@fancyweb
Copy link
Contributor Author

fancyweb commented Jan 10, 2017

OK I really understand your point of view (I'm already a reader of your blog). In my case, the reusable bundles have domain logic because they are "private" bundles used only by the projects of my company, and we are making websites about one domain only. I guess a bundle doesn't have to be infrastructural to be a bundle, sometimes you want domain logic inside voluntarily.

@weaverryan
Copy link
Member

So, could we close this? I'm also not convinced there is enough need here: you need to make your voters work together, they are definitely your business/domain logic.

I'm 👎

@fabpot
Copy link
Member

fabpot commented Sep 27, 2017

I'm also not convinced about this change. Let's close.

@fabpot fabpot closed this Sep 27, 2017
@fancyweb fancyweb deleted the access-decision-strategy-resolver branch August 9, 2019 07:12
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