The Symfony Security Component has an AccessDecisionManager which decides whether or not the currently authenticated user has a right to be in some place (or for that matter, use a certain service from the service container, or even call a certain method). The decision manager looks at the current user's roles, and compares them to the attributes that are required. It relies on dedicated voters to make it's verdict.

The component itself ships with an AuthenticatedVoter. It supports the "IS_AUTHENTICATED_FULLY", "IS_AUTHENTICATED_REMEMBERED" and "IS_AUTHENTICATED_ANONYMOUSLY" attributes, which allow you to differentiate between users who are authenticated in the normal way, via a "remember me" cookie, or anonymously (which means: no credentials were supplied, but the user still gets a security context).

The other readily available voters are the RoleVoter and the RoleHierarchyVoter, which support the "ROLE_*" attributes. The first voter just looks at an attribute and checks whether the current user has this attribute, the second voter takes a hierarchy of roles, and checks for all reachable roles for a given role (e.g. in the default security configuration from the Symfony2 standard edition, "ROLE_SUPER_ADMIN" includes the roles "ROLE_USER", "ROLE_ADMIN" and "ROLE_ALLOWED_TO_SWITCH").

It is also possible to create your own voter, as shown in various posts around the Internet and also in the Symfony Cookbook. Yet, I wanted to write about one specific voter I have implemented using Silex (it now natively supports integration with the Security Component). It brings out it's vote based on the domain of the current request.

The DomainVoter

Each voter should implement VoterInterface which requires you to define a vote() method. This method receives the current token (which contains the current user), the object that should be decided on, and a set of attributes that are required. The only thing this voter knows anything about is the Request and the User (which implements UserInterface). Thus the voter should abstain from voting when it is provided with anything else. Also, as it is commonly good practice to separate concerns, I have decided to put the decision mechanism itself outside the voter, in a DomainAccessDecisionManager class. This allows for reusability.

namespace Matthias\Security;

use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Matthias\Security\DomainAccessDecisionManagerInterface;

class DomainVoter implements VoterInterface
{
    const ROLE_ALLOWED_ON_DOMAIN = 'ALLOWED_ON_DOMAIN';

    private $decisionManager;

    public function __construct(DomainAccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    public function supportsAttribute($attribute)
    {
        return self::ROLE_ALLOWED_ON_DOMAIN === $attribute;
    }

    public function supportsClass($class)
    {
        return true;
    }

    public function vote(TokenInterface $token, $object, array $attributes)
    {
        $result = self::ACCESS_ABSTAIN;

        if (!($object instanceof Request)) {
            return $result;
        }

        $user = $token->getUser();
        if (!($user instanceof UserInterface)) {
            return $result;
        }

        /* @var $object Request */

        foreach ($attributes as $attribute) {
            // these attributes come from the access control rules in the security configuration
            if (!$this->supportsAttribute($attribute)) {
                continue;
            }

            $host = $object->getHost();
            $user = $token->getUser();

            if ($this->decisionManager->decide($user, $host)) {
                $result = self::ACCESS_GRANTED;
            }
            else {
                $result = self::ACCESS_DENIED;
            }
        }

        return $result;
    }
}

The code for the DomainAccessDecisionManager is quite simple, yet also very extendable. You may implement a manager which fetches the list of domain name => usernames[] from a database or something.

namespace Matthias\Security;

use Symfony\Component\Security\Core\User\UserInterface;
use Matthias\Security\DomainAccessDecisionManagerInterface;

class DomainAccessDecisionManager implements DomainAccessDecisionManagerInterface
{
    private $domains;

    public function __construct(array $domainsAndUsers = array())
    {
        foreach ($domainsAndUsers as $domain => $users) {
            $this->domains[$domain] = (array) $users;
        }
    }

    public function decide(UserInterface $user, $host)
    {
        if (!is_string($host)) {
            throw new \InvalidArgumentException('$host should be a string');
        }

        if (!isset($this->domains[$host])) {
            return false;
        }

        return in_array($user->getUsername(), $this->domains[$host]);
    }
}

To allow for the above-mentioned extensibility, the manager implements DomainAccessDecisionManagerInterface:

namespace Matthias\Security;

use Symfony\Component\Security\Core\User\UserInterface;

interface DomainAccessDecisionManagerInterface
{
    public function decide(UserInterface $user, $host);
}

Putting it all together, the Silex way

To make the voter work in a Silex application, we have to create services for the DomainAccessDecisionManager and the DomainVoter, add a parameter which contains the list of domains and usernames and also extend the list of security voters:

use Matthias\Security\DomainVoter;
use Matthias\Security\DomainAccessDecisionManager;

// ...

$app['domains_and_users'] = array(
    'mysubdomain.thedomain.org' => array('admin', 'matthias'),
    'othersubdomain.thedomain.org' => array('admin'),
);

$app['domain_decision_manager'] = $app->share(function($app) {
    return new DomainAccessDecisionManager($app['domains_and_users']);
});

$app['domain_voter'] = $app->share(function($app) {
    return new DomainVoter($app['domain_decision_manager']);
});

$app['security.voters'] = $app->extend('security.voters', function($voters) use ($app) {
    $voters[] = $app['domain_voter'];

    return $voters;
});

Finally, we need to change the strategy of the AccessDecisionManager to be "unanimous", since we want it to take the vote of the DomainVoter very seriously (using the default "affirmative" strategy, it would be sufficient to grant access to the user if any voter would return ACCESS_GRANTED).

Due to a limitation in the way the SecurityServiceProvider currently sets up all the dependencies, the strategy of the AccessDecisionManager can not be configured without redefining the service itself:

use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;

// ...

$app['security.access_manager'] = $app->share(function($app) {
    return new AccessDecisionManager($app['security.voters'], 'unanimous');
});
PHP Security Silex Symfony2 roles voters
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
cordoval

pretty crazy stuff, nice man