From acf66f9161ea02df8ab885db682dab6a5851f96b Mon Sep 17 00:00:00 2001 From: WouterJ Date: Sat, 27 Jun 2015 20:13:50 +0200 Subject: [PATCH 1/3] Move access decision strategy section --- cookbook/security/voters.rst | 83 -------------------- cookbook/security/voters_data_permission.rst | 82 +++++++++++++++++++ 2 files changed, 82 insertions(+), 83 deletions(-) diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 4bc7df40a3a..4a3cd926d83 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -149,86 +149,3 @@ and tag it as a ``security.voter``: configuration file (e.g. ``app/config/config.yml``). For more information see :ref:`service-container-imports-directive`. To read more about defining services in general, see the :doc:`/book/service_container` chapter. - -.. _security-voters-change-strategy: - -Changing the Access Decision Strategy -------------------------------------- - -In order for the new voter to take effect, you need to change the default access -decision strategy, which, by default, grants access if *any* voter grants -access. - -In this case, choose the ``unanimous`` strategy. Unlike the ``affirmative`` -strategy (the default), with the ``unanimous`` strategy, if only one voter -denies access (e.g. the ``ClientIpVoter``), access is not granted to the -end user. - -To do that, override the default ``access_decision_manager`` section of your -application configuration file with the following code. - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - access_decision_manager: - # strategy can be: affirmative, unanimous or consensus - strategy: unanimous - - .. code-block:: xml - - - - - - - - .. code-block:: php - - // app/config/security.xml - $container->loadFromExtension('security', array( - // strategy can be: affirmative, unanimous or consensus - 'access_decision_manager' => array( - 'strategy' => 'unanimous', - ), - )); - -That's it! Now, when deciding whether or not a user should have access, -the new voter will deny access to any user in the list of blacklisted IPs. - -Note that the voters are only called, if any access is actually checked. So -you need at least something like - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - access_control: - - { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.xml - $container->loadFromExtension('security', array( - 'access_control' => array( - array('path' => '^/', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), - ), - )); - -.. seealso:: - - For a more advanced usage see - :ref:`components-security-access-decision-manager`. diff --git a/cookbook/security/voters_data_permission.rst b/cookbook/security/voters_data_permission.rst index 2376d393b53..240035c3289 100644 --- a/cookbook/security/voters_data_permission.rst +++ b/cookbook/security/voters_data_permission.rst @@ -220,3 +220,85 @@ from the security context is called. } It's that easy! + +.. _security-voters-change-strategy: + +Changing the Access Decision Strategy +------------------------------------- + +In order for the new voter to take effect, you need to change the default access +decision strategy, which, by default, grants access if *any* voter grants +access. + +In this case, choose the ``unanimous`` strategy. Unlike the ``affirmative`` +strategy (the default), with the ``unanimous`` strategy, if only one voter +denies access (e.g. the ``ClientIpVoter``), access is not granted to the +end user. + +To do that, override the default ``access_decision_manager`` section of your +application configuration file with the following code. + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + access_decision_manager: + # strategy can be: affirmative, unanimous or consensus + strategy: unanimous + + .. code-block:: xml + + + + + + + + .. code-block:: php + + // app/config/security.xml + $container->loadFromExtension('security', array( + // strategy can be: affirmative, unanimous or consensus + 'access_decision_manager' => array( + 'strategy' => 'unanimous', + ), + )); + +That's it! Now, when deciding whether or not a user should have access, +the new voter will deny access to any user in the list of blacklisted IPs. + +Note that the voters are only called, if any access is actually checked. So +you need at least something like + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + access_control: + - { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY } + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // app/config/security.xml + $container->loadFromExtension('security', array( + 'access_control' => array( + array('path' => '^/', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), + ), + )); + +.. seealso:: + + For a more advanced usage see :ref:`components-security-access-decision-manager`. From 9c169c79b96eba90735a7e31488a0223aa443676 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Sat, 27 Jun 2015 20:28:49 +0200 Subject: [PATCH 2/3] Rewrite new section --- cookbook/security/voters_data_permission.rst | 90 ++++++++------------ 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/cookbook/security/voters_data_permission.rst b/cookbook/security/voters_data_permission.rst index 240035c3289..3513b6dde0b 100644 --- a/cookbook/security/voters_data_permission.rst +++ b/cookbook/security/voters_data_permission.rst @@ -223,20 +223,30 @@ It's that easy! .. _security-voters-change-strategy: -Changing the Access Decision Strategy -------------------------------------- +Changing the Decision Strategy +------------------------------ -In order for the new voter to take effect, you need to change the default access -decision strategy, which, by default, grants access if *any* voter grants -access. +Imagine you have multiple voters for one action for an object. For instance, +you have one voter that checks if the user is a member of the site and a second +one checking if the user is older than 18. -In this case, choose the ``unanimous`` strategy. Unlike the ``affirmative`` -strategy (the default), with the ``unanimous`` strategy, if only one voter -denies access (e.g. the ``ClientIpVoter``), access is not granted to the -end user. +To handle these cases, the access decision manager uses a decision strategy. +You can configure this to suite your needs. There are three strategies +available: -To do that, override the default ``access_decision_manager`` section of your -application configuration file with the following code. +``affirmative`` (default) + This grants access as soon as there is *one* voter granting access; + +``consensus`` + This grants access if there are more voters granting access than denying; + +``unanimous`` + This only grants access once *all* voters grant access. + +In the above scenario, both voters should grant access in order to grant access +to the user to read the post. In this case, the default strategy is no longer +valid and ``unanimous`` should be used instead. You can set this in the +security configuration: .. configuration-block:: @@ -245,60 +255,30 @@ application configuration file with the following code. # app/config/security.yml security: access_decision_manager: - # strategy can be: affirmative, unanimous or consensus strategy: unanimous .. code-block:: xml - - - - + + + + + + .. code-block:: php - // app/config/security.xml + // app/config/security.php $container->loadFromExtension('security', array( - // strategy can be: affirmative, unanimous or consensus 'access_decision_manager' => array( 'strategy' => 'unanimous', ), )); - -That's it! Now, when deciding whether or not a user should have access, -the new voter will deny access to any user in the list of blacklisted IPs. - -Note that the voters are only called, if any access is actually checked. So -you need at least something like - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - access_control: - - { path: ^/, role: IS_AUTHENTICATED_ANONYMOUSLY } - - .. code-block:: xml - - - - - - - - - .. code-block:: php - - // app/config/security.xml - $container->loadFromExtension('security', array( - 'access_control' => array( - array('path' => '^/', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), - ), - )); - -.. seealso:: - - For a more advanced usage see :ref:`components-security-access-decision-manager`. From 93484a707df22b92ed3e6ca478e7ce996b863284 Mon Sep 17 00:00:00 2001 From: WouterJ Date: Sat, 27 Jun 2015 20:39:20 +0200 Subject: [PATCH 3/3] Remove the old voter article --- book/security.rst | 10 +- cookbook/map.rst.inc | 1 - cookbook/security/acl.rst | 2 +- cookbook/security/index.rst | 1 - cookbook/security/voter_interface.rst.inc | 24 -- cookbook/security/voters.rst | 332 ++++++++++++++----- cookbook/security/voters_data_permission.rst | 284 ---------------- redirection_map | 1 + 8 files changed, 248 insertions(+), 407 deletions(-) delete mode 100644 cookbook/security/voter_interface.rst.inc delete mode 100644 cookbook/security/voters_data_permission.rst diff --git a/book/security.rst b/book/security.rst index 9bfe3c2809a..640152cfda2 100644 --- a/book/security.rst +++ b/book/security.rst @@ -929,10 +929,10 @@ other users. Also, as the admin user, you yourself want to be able to edit To accomplish this you have 2 options: -* :doc:`Voters ` allow you to - use business logic (e.g. the user can edit this post because they were - the creator) to determine access. You'll probably want this option - it's - flexible enough to solve the above situation. +* :doc:`Voters ` allow you to use business logic + (e.g. the user can edit this post because they were the creator) to determine + access. You'll probably want this option - it's flexible enough to solve the + above situation. * :doc:`ACLs ` allow you to create a database structure where you can assign *any* arbitrary user *any* access (e.g. EDIT, VIEW) @@ -1281,7 +1281,7 @@ Learn More from the Cookbook * :doc:`Forcing HTTP/HTTPS ` * :doc:`Impersonating a User ` -* :doc:`/cookbook/security/voters_data_permission` +* :doc:`/cookbook/security/voters` * :doc:`Access Control Lists (ACLs) ` * :doc:`/cookbook/security/remember_me` * :doc:`/cookbook/security/multiple_user_providers` diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index 6c37de6d5f7..135342503b7 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -151,7 +151,6 @@ * :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 c6313167c40..507efe00dd7 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 5bf643c10e8..e666a2dcff4 100644 --- a/cookbook/security/index.rst +++ b/cookbook/security/index.rst @@ -9,7 +9,6 @@ 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 deleted file mode 100644 index 1a3cd989e3c..00000000000 --- a/cookbook/security/voter_interface.rst.inc +++ /dev/null @@ -1,24 +0,0 @@ -.. 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 like ``ROLE_USER``, an ACL ``EDIT``, etc.). - -The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass` -method is used to check if the voter supports the class of the object whose -access is being checked. - -The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::vote` -method must implement the business logic that verifies whether or not the -user has 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. diff --git a/cookbook/security/voters.rst b/cookbook/security/voters.rst index 4a3cd926d83..7025d61ae3e 100644 --- a/cookbook/security/voters.rst +++ b/cookbook/security/voters.rst @@ -1,151 +1,301 @@ .. index:: - single: Security; Voters + single: Security; Data Permission + single: Security: Voters -How to Implement your own Voter to Blacklist IP Addresses -========================================================= +How to Use Voters to Check User Permissions +=========================================== -The Symfony Security component provides several layers to authorize users. -One of the layers is called a "voter". A voter is a dedicated class that checks -if the user has the rights to connect to the application or access a specific -resource/URL. For instance, Symfony provides a layer that checks if the user -is fully authorized or if it has some expected roles. +In Symfony, 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. -It is sometimes useful to create a custom voter to handle a specific case not -handled by the framework. In this section, you'll learn how to create a voter -that will allow you to blacklist users by their IP. +.. tip:: + + Take a look at the + :doc:`authorization ` + chapter for an even deeper understanding on voters. + +How Symfony Uses Voters +----------------------- + +In order to use voters, you have to understand how Symfony works with them. +All voters are called each time you use the ``isGranted()`` method on Symfony's +security context (i.e. the ``security.context`` service). Each one decides +if the current user should have access to some resource. + +Ultimately, Symfony uses one of three different approaches on what to do +with the feedback from all voters: affirmative, consensus and unanimous. + +For more information take 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 requires the following three methods: +which has this structure:: -.. include:: /cookbook/security/voter_interface.rst.inc + interface VoterInterface + { + public function supportsAttribute($attribute); + public function supportsClass($class); + public function vote(TokenInterface $token, $object, array $attributes); + } -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 -``VoterInterface::ACCESS_DENIED``, otherwise you'll return -``VoterInterface::ACCESS_ABSTAIN`` as this voter's purpose is only to deny -access, not to grant access. +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 like ``ROLE_USER``, an ACL ``EDIT``, etc.). -Creating a custom Voter ------------------------ +The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass` +method is used to check if the voter supports the class of the object whose +access is being checked. -To blacklist a user based on its IP, you can use the ``request`` service -and compare the IP address against a set of blacklisted IP addresses: +The :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::vote` +method must implement the business logic that verifies whether or not the +user has access. This method must return one of the following values: -.. code-block:: php +* ``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. + +In this example, the voter will check if the user has 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 +------------------------- - // src/AppBundle/Security/Authorization/Voter/ClientIpVoter.php +The goal is to create a voter that checks if a user has access to view or +edit a particular object. Here's an example implementation:: + + // src/AppBundle/Security/Authorization/Voter/PostVoter.php namespace AppBundle\Security\Authorization\Voter; - 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; - class ClientIpVoter implements VoterInterface + class PostVoter implements VoterInterface { - private $container; - - private $blacklistedIp; - - public function __construct(ContainerInterface $container, array $blacklistedIp = array()) - { - $this->container = $container; - $this->blacklistedIp = $blacklistedIp; - } + const VIEW = 'view'; + const EDIT = 'edit'; public function supportsAttribute($attribute) { - // you won't check against a user attribute, so return true - return true; + return in_array($attribute, array( + self::VIEW, + self::EDIT, + )); } public function supportsClass($class) { - // your voter supports all type of token classes, so return true - return true; + $supportedClass = 'AppBundle\Entity\Post'; + + return $supportedClass === $class || is_subclass_of($class, $supportedClass); } - public function vote(TokenInterface $token, $object, array $attributes) + /** + * @var \AppBundle\Entity\Post $post + */ + public function vote(TokenInterface $token, $post, array $attributes) { - $request = $this->container->get('request'); - if (in_array($request->getClientIp(), $this->blacklistedIp)) { + // check if class of this object is supported by this voter + if (!$this->supportsClass(get_class($post))) { + return VoterInterface::ACCESS_ABSTAIN; + } + + // check if the voter is used correct, only allow one attribute + // this isn't a requirement, it's just one easy way for you to + // design your voter + if (1 !== count($attributes)) { + throw new \InvalidArgumentException( + 'Only one attribute is allowed for VIEW or EDIT' + ); + } + + // set the attribute to check against + $attribute = $attributes[0]; + + // check if the given attribute is covered by this voter + if (!$this->supportsAttribute($attribute)) { + return VoterInterface::ACCESS_ABSTAIN; + } + + // get current logged in user + $user = $token->getUser(); + + // make sure there is a user object (i.e. that the user is logged in) + if (!$user instanceof UserInterface) { return VoterInterface::ACCESS_DENIED; } - return VoterInterface::ACCESS_ABSTAIN; + switch($attribute) { + case self::VIEW: + // the data object could have for example a method isPrivate() + // which checks the boolean attribute $private + if (!$post->isPrivate()) { + return VoterInterface::ACCESS_GRANTED; + } + break; + + case self::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; + } + + return VoterInterface::ACCESS_DENIED; } } That's it! The voter is done. The next step is to inject the voter into -the security layer. This can be done easily through the service container. - -.. tip:: - - Your implementation of the methods - :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsAttribute` - and :method:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::supportsClass` - are not being called internally by the framework. Once you have registered your - voter the ``vote()`` method will always be called, regardless of whether - or not these two methods return true. Therefore you need to call those - methods in your implementation of the ``vote()`` method and return ``ACCESS_ABSTAIN`` - if your voter does not support the class or attribute. +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``: +To inject the voter into the security layer, you must declare it as a service +and tag it with ``security.voter``: .. configuration-block:: .. code-block:: yaml - # src/Acme/AcmeBundle/Resources/config/services.yml + # src/AppBundle/Resources/config/services.yml services: - security.access.blacklist_voter: - class: AppBundle\Security\Authorization\Voter\ClientIpVoter - arguments: ["@service_container", [123.123.123.123, 171.171.171.171]] - public: false + security.access.post_voter: + class: AppBundle\Security\Authorization\Voter\PostVoter + public: false tags: - - { name: security.voter } + - { name: security.voter } .. code-block:: xml - - - - - 123.123.123.123 - 171.171.171.171 - - - + + + + + + + + + .. code-block:: php - // src/Acme/AcmeBundle/Resources/config/services.php - use Symfony\Component\DependencyInjection\Definition; - use Symfony\Component\DependencyInjection\Reference; + // src/AppBundle/Resources/config/services.php + $container + ->register( + 'security.access.post_document_voter', + 'AppBundle\Security\Authorization\Voter\PostVoter' + ) + ->addTag('security.voter') + ; - $definition = new Definition( - 'AppBundle\Security\Authorization\Voter\ClientIpVoter', - array( - new Reference('service_container'), - array('123.123.123.123', '171.171.171.171'), - ), - ); - $definition->addTag('security.voter'); - $definition->setPublic(false); +How to Use the Voter in a Controller +------------------------------------ - $container->setDefinition('security.access.blacklist_voter', $definition); +The registered voter will then always be asked as soon as the method ``isGranted()`` +from the security context is called. -.. tip:: +.. code-block:: php + + // src/AppBundle/Controller/PostController.php + namespace AppBundle\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($id) + { + // 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().'

'); + } + } + +It's that easy! + +.. _security-voters-change-strategy: + +Changing the Access Decision Strategy +------------------------------------- + +Imagine you have multiple voters for one action for an object. For instance, +you have one voter that checks if the user is a member of the site and a second +one checking if the user is older than 18. - Be sure to import this configuration file from your main application - configuration file (e.g. ``app/config/config.yml``). For more information - see :ref:`service-container-imports-directive`. To read more about defining - services in general, see the :doc:`/book/service_container` chapter. +To handle these cases, the access decision manager uses an access decision +strategy. You can configure this to suite your needs. There are three +strategies available: + +``affirmative`` (default) + This grants access as soon as there is *one* voter granting access; + +``consensus`` + This grants access if there are more voters granting access than denying; + +``unanimous`` + This only grants access once *all* voters grant access. + +In the above scenario, both voters should grant access in order to grant access +to the user to read the post. In this case, the default strategy is no longer +valid and ``unanimous`` should be used instead. You can set this in the +security configuration: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + access_decision_manager: + strategy: unanimous + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // app/config/security.php + $container->loadFromExtension('security', array( + 'access_decision_manager' => array( + 'strategy' => 'unanimous', + ), + )); diff --git a/cookbook/security/voters_data_permission.rst b/cookbook/security/voters_data_permission.rst deleted file mode 100644 index 3513b6dde0b..00000000000 --- a/cookbook/security/voters_data_permission.rst +++ /dev/null @@ -1,284 +0,0 @@ -.. index:: - single: Security; Data Permission Voters - -How to Use Voters to Check User Permissions -=========================================== - -In Symfony, 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. - -.. seealso:: - - Voters can also be used in other ways, like, for example, blacklisting IP - addresses from the entire application: :doc:`/cookbook/security/voters`. - -.. tip:: - - Take a look at the - :doc:`authorization ` - chapter for an even deeper understanding on voters. - -How Symfony Uses Voters ------------------------ - -In order to use voters, you have to understand how Symfony works with them. -All voters are called each time you use the ``isGranted()`` method on Symfony's -security context (i.e. the ``security.context`` service). Each one decides -if the current user should have access to some resource. - -Ultimately, Symfony uses one of three different approaches on what to do -with the feedback from all voters: affirmative, consensus and unanimous. - -For more information take 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, the voter will check if the user has 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 -------------------------- - -The goal is to create a voter that checks if a user has access to view or -edit a particular object. Here's an example implementation:: - - // src/AppBundle/Security/Authorization/Voter/PostVoter.php - namespace AppBundle\Security\Authorization\Voter; - - use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\User\UserInterface; - - 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($class) - { - $supportedClass = 'AppBundle\Entity\Post'; - - return $supportedClass === $class || is_subclass_of($class, $supportedClass); - } - - /** - * @var \AppBundle\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(get_class($post))) { - return VoterInterface::ACCESS_ABSTAIN; - } - - // check if the voter is used correct, only allow one attribute - // this isn't a requirement, it's just one easy way for you to - // design your voter - if (1 !== count($attributes)) { - throw new \InvalidArgumentException( - 'Only one attribute is allowed for VIEW or EDIT' - ); - } - - // set the attribute to check against - $attribute = $attributes[0]; - - // check if the given attribute is covered by this voter - if (!$this->supportsAttribute($attribute)) { - return VoterInterface::ACCESS_ABSTAIN; - } - - // get current logged in user - $user = $token->getUser(); - - // make sure there is a user object (i.e. that the user is logged in) - if (!$user instanceof UserInterface) { - return VoterInterface::ACCESS_DENIED; - } - - switch($attribute) { - case self::VIEW: - // the data object could have for example a method isPrivate() - // which checks the boolean attribute $private - if (!$post->isPrivate()) { - return VoterInterface::ACCESS_GRANTED; - } - break; - - case self::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; - } - - return VoterInterface::ACCESS_DENIED; - } - } - -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 with ``security.voter``: - -.. configuration-block:: - - .. code-block:: yaml - - # src/AppBundle/Resources/config/services.yml - services: - security.access.post_voter: - class: AppBundle\Security\Authorization\Voter\PostVoter - public: false - tags: - - { name: security.voter } - - .. code-block:: xml - - - - - - - - - - - - .. code-block:: php - - // src/AppBundle/Resources/config/services.php - $container - ->register( - 'security.access.post_document_voter', - 'AppBundle\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/AppBundle/Controller/PostController.php - namespace AppBundle\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($id) - { - // 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().'

'); - } - } - -It's that easy! - -.. _security-voters-change-strategy: - -Changing the Decision Strategy ------------------------------- - -Imagine you have multiple voters for one action for an object. For instance, -you have one voter that checks if the user is a member of the site and a second -one checking if the user is older than 18. - -To handle these cases, the access decision manager uses a decision strategy. -You can configure this to suite your needs. There are three strategies -available: - -``affirmative`` (default) - This grants access as soon as there is *one* voter granting access; - -``consensus`` - This grants access if there are more voters granting access than denying; - -``unanimous`` - This only grants access once *all* voters grant access. - -In the above scenario, both voters should grant access in order to grant access -to the user to read the post. In this case, the default strategy is no longer -valid and ``unanimous`` should be used instead. You can set this in the -security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # app/config/security.yml - security: - access_decision_manager: - strategy: unanimous - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // app/config/security.php - $container->loadFromExtension('security', array( - 'access_decision_manager' => array( - 'strategy' => 'unanimous', - ), - )); diff --git a/redirection_map b/redirection_map index 7e60794f8c6..1ebc69ced38 100644 --- a/redirection_map +++ b/redirection_map @@ -25,3 +25,4 @@ /components/yaml /components/yaml/introduction /components/templating /components/templating/introduction /cookbook/upgrading /cookbook/upgrade/index +/cookbook/security/voters_data_permission /cookbook/security/voters