diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index 6986e4b1ca0..0e5d08ed344 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -132,6 +132,7 @@ * :doc:`/cookbook/security/remember_me` * :doc:`/cookbook/security/impersonating_user` * :doc:`/cookbook/security/voters` + * :doc:`/cookbook/security/voters_data_permission` * :doc:`/cookbook/security/acl` * :doc:`/cookbook/security/acl_advanced` * :doc:`/cookbook/security/force_https` diff --git a/cookbook/security/acl.rst b/cookbook/security/acl.rst index 71cd6537ad4..aba1b1ff99f 100644 --- a/cookbook/security/acl.rst +++ b/cookbook/security/acl.rst @@ -14,7 +14,7 @@ the ACL system comes in. Using ACL's isn't trivial, and for simpler use cases, it may be overkill. If your permission logic could be described by just writing some code (e.g. to check if a Blog is owned by the current User), then consider using - :doc:`voters `. A voter is passed the object + :doc:`voters `. A voter is passed the object being voted on, which you can use to make complex decisions and effectively implement your own ACL. Enforcing authorization (e.g. the ``isGranted`` part) will look similar to what you see in this entry, but your voter diff --git a/cookbook/security/index.rst b/cookbook/security/index.rst index 63bd29520b3..40b0edd297c 100644 --- a/cookbook/security/index.rst +++ b/cookbook/security/index.rst @@ -8,6 +8,7 @@ Security remember_me impersonating_user voters + voters_data_permission acl acl_advanced force_https diff --git a/cookbook/security/voter_interface.rst.inc b/cookbook/security/voter_interface.rst.inc new file mode 100644 index 00000000000..3bd3a5de819 --- /dev/null +++ b/cookbook/security/voter_interface.rst.inc @@ -0,0 +1,26 @@ +.. code-block:: php + + interface VoterInterface + { + public function supportsAttribute($attribute); + public function supportsClass($class); + public function vote(TokenInterface $token, object, array $attributes); + } + +The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsAttribute` +method is used to check if the voter supports the given user attribute (i.e: a role, an acl, etc.). + +The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass` +method is used to check if the voter supports the current user token class. + +The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::vote` +method must implement the business logic that verifies whether or not the +user is granted access. This method must return one of the following values: + +* ``VoterInterface::ACCESS_GRANTED``: The user is allowed to access the + application +* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if the user + is granted or not +* ``VoterInterface::ACCESS_DENIED``: The user is not allowed to access + the application + diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 8798bcefa06..394fbb71893 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -21,28 +21,7 @@ A custom voter must implement :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, which requires the following three methods: -.. code-block:: php - - interface VoterInterface - { - public function supportsAttribute($attribute); - public function supportsClass($class); - public function vote(TokenInterface $token, $object, array $attributes); - } - -The ``supportsAttribute()`` method is used to check if the voter supports -the given user attribute (i.e: a role, an ACL, etc.). - -The ``supportsClass()`` method is used to check if the voter supports the -class of the object whose access is being checked (doesn't apply to this entry). - -The ``vote()`` method must implement the business logic that verifies whether -or not the user is granted access. This method must return one of the following -values: - -* ``VoterInterface::ACCESS_GRANTED``: The authorization will be granted by this voter; -* ``VoterInterface::ACCESS_ABSTAIN``: The voter cannot decide if authorization should be granted; -* ``VoterInterface::ACCESS_DENIED``: The authorization will be denied by this voter. +.. include:: /cookbook/security/voter_interface.rst.inc In this example, you'll check if the user's IP address matches against a list of blacklisted addresses and "something" will be the application. If the user's IP is blacklisted, you'll return diff --git a/cookbook/security/voters_data_permission.rst b/cookbook/security/voters_data_permission.rst new file mode 100644 index 00000000000..9d68eee0998 --- /dev/null +++ b/cookbook/security/voters_data_permission.rst @@ -0,0 +1,210 @@ +.. index:: + single: Security; Data Permission Voters + +How to Use Voters to Check User Permissions +=========================================== + +In Symfony2 you can check the permission to access data by using the +:doc:`ACL module `, which is a bit overwhelming +for many applications. A much easier solution is to work with custom voters, +which are like simple conditional statements. Voters can also be used to +check for permission to a part or even of the whole application: +":doc:`/cookbook/security/voters`". + +.. tip:: + + Have a look at the + :doc:`authorization ` + chapter for a better understanding on voters. + +How Symfony Uses Voters +----------------------- + +In order to use voters, you have to understand how Symfony works with them. +In general, all registered custom voters will be called every time you ask +Symfony about permissions (ACL). You can use one of three different +approaches on how to handle the feedback from all voters: affirmative, +consensus and unanimous. For more information have a look at +":ref:`the section about access decision managers `". + +The Voter Interface +------------------- + +A custom voter must implement +:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface`, +which has this structure: + +.. include:: /cookbook/security/voter_interface.rst.inc + +In this example, it'll check if the user will have access to a specific +object according to your custom conditions (e.g. they must be the owner of +the object). If the condition fails, you'll return +``VoterInterface::ACCESS_DENIED``, otherwise you'll return +``VoterInterface::ACCESS_GRANTED``. In case the responsibility for this decision +does not belong to this voter, it will return ``VoterInterface::ACCESS_ABSTAIN``. + +Creating the Custom Voter +------------------------- + +You could implement your Voter to check permission for the view and edit action like the following:: + + // src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php + namespace Acme\DemoBundle\Security\Authorization\Voter; + + use Symfony\Component\Security\Core\Exception\InvalidArgumentException; + use Symfony\Component\DependencyInjection\ContainerInterface; + use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\User\UserInterface; + use Acme\DemoBundle\Entity\Post; + + class PostVoter implements VoterInterface + { + const VIEW = 'view'; + const EDIT = 'edit'; + + public function supportsAttribute($attribute) + { + return in_array($attribute, array( + self::VIEW, + self::EDIT, + )); + } + + public function supportsClass($obj) + { + return $obj instanceof Post; + } + + /** + * @var \Acme\DemoBundle\Entity\Post $post + */ + public function vote(TokenInterface $token, $post, array $attributes) + { + // check if class of this object is supported by this voter + if (!$this->supportsClass($post)) { + return VoterInterface::ACCESS_ABSTAIN; + } + + // check if voter is used correct, only allow one attribute for a check + if(1 !== count($attributes) || !is_string($attributes[0])) { + throw new InvalidArgumentException( + 'Only one attribute is allowed for VIEW or EDIT' + ); + } + + // set the attribute to check against + $attribute = $attributes[0]; + + // get current logged in user + $user = $token->getUser(); + + // check if the given attribute is covered by this voter + if (!$this->supportsAttribute($attribute)) { + return VoterInterface::ACCESS_ABSTAIN; + } + + // check if given user is instance of user interface + if (!$user instanceof UserInterface) { + return VoterInterface::ACCESS_DENIED; + } + + switch($attribute) { + case 'view': + // the data object could have for e.g. a method isPrivate() + // which checks the Boolean attribute $private + if (!$post->isPrivate()) { + return VoterInterface::ACCESS_GRANTED; + } + break; + + case 'edit': + // we assume that our data object has a method getOwner() to + // get the current owner user entity for this data object + if ($user->getId() === $post->getOwner()->getId()) { + return VoterInterface::ACCESS_GRANTED; + } + break; + } + + } + } + +That's it! The voter is done. The next step is to inject the voter into +the security layer. + +Declaring the Voter as a Service +-------------------------------- + +To inject the voter into the security layer, you must declare it as a service +and tag it as a ``security.voter``: + +.. configuration-block:: + + .. code-block:: yaml + + # src/Acme/DemoBundle/Resources/config/services.yml + services: + security.access.post_voter: + class: Acme\DemoBundle\Security\Authorization\Voter\PostVoter + public: false + tags: + - { name: security.voter } + + .. code-block:: xml + + + + + + + + + + + + .. code-block:: php + + // src/Acme/DemoBundle/Resources/config/services.php + $container + ->register( + 'security.access.post_document_voter', + 'Acme\DemoBundle\Security\Authorization\Voter\PostVoter' + ) + ->addTag('security.voter') + ; + +How to Use the Voter in a Controller +------------------------------------ + +The registered voter will then always be asked as soon as the method ``isGranted()`` +from the security context is called. + +.. code-block:: php + + // src/Acme/DemoBundle/Controller/PostController.php + namespace Acme\DemoBundle\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Security\Core\Exception\AccessDeniedException; + + class PostController extends Controller + { + public function showAction() + { + // get a Post instance + $post = ...; + + // keep in mind, this will call all registered security voters + if (false === $this->get('security.context')->isGranted('view', $post)) { + throw new AccessDeniedException('Unauthorised access!'); + } + + return new Response('

'.$post->getName().'

'); + } + }