MattW
MattW

Reputation: 13212

Symfony2: Call Voter from another Voter

I am using Voters to restrict access to entities in a REST API.

Step 1

Consider this voter that restricts users access to blog posts:

class BlogPostVoter extends Voter
{
    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    /**
     * Determines if the attribute and subject are supported by this voter.
     *
     * @param string $attribute An attribute
     * @param int $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
     *
     * @return bool True if the attribute and subject are supported, false otherwise
     */
    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, $this->allowedAttributes)) {
            return false;
        }
        if (!$subject instanceof BlogPost) {
            return false;
        }

        return true;
    }

    /**
     * Perform a single access check operation on a given attribute, subject and token.
     *
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     * @return bool
     * @throws \Exception
     */
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        return $this->canUserAccess($attribute, $subject, $token);
    }

    public function canUserAccess($attribute, $subject, TokenInterface $token) {
        if ($this->decisionManager->decide($token, array('ROLE_SUPPORT', 'ROLE_ADMIN'))) {
            return true;
        } 

        //other logic here omitted ...

        return false;
    }
}

You can see there is a public function canUserAccess to determine if the user is allowed to see the BlogPost. This all works just fine.

Step 2

Now I have another voter that checks something else, but also needs to check this same exact logic for BlogPosts. My thought was to:

So I thought I would inject the BlogPostVoter into my other voter like this:

class SomeOtherVoter extends Voter
{
    public function __construct(BlogPostVoter $blogPostVoter)
    {
        $this->decisionManager = $decisionManager;
    }

    ...

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        //other logic

        if ($this->blogPostVoter->canUserAccess($attribute, $subject, $token))    {
           return true;
        }

        return false;
    }
}

Problem

When I do this I get the following error, using both setter and constructor injection:

Circular reference detected for service "security.access.decision_manager", path: "security.access.decision_manager"

I don't see where the security.access.decision_manager should depend on the Voter implementations. So I'm not seeing where the circular reference is.

Is there another way I can call VoterA from VoterB?

Upvotes: 4

Views: 1241

Answers (1)

Kim
Kim

Reputation: 1888

To reference VoterOne from VoterTwo you can inject the AuthorizationCheckerInterface into VoterTwo and then call ->isGranted('ONE'). Where ONE is the supported attribute of VoterOne.

Like:

class VoterTwo extends Voter
{
    private $authorizationChecker;

    public function __construct(AuthorizationCheckerInterface $authorizationChecker)
    {
        $this->authorizationChecker = $authorizationChecker;
    }

    protected function supports($attribute, $subject)
    {
        return in_array($attribute, ['TWO']);
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        return $this->authorizationChecker->isGranted('ONE', $subject);
    }
}

In this example VoterTwo does just redirect the request to VoterOne (or the voter that supports the attribute ONE). This can then be extended through additional conditions.

Upvotes: 3

Related Questions