From e4a29a620a919083993f893b87ff6fd88a7db9c9 Mon Sep 17 00:00:00 2001 From: michnovka Date: Tue, 1 Oct 2024 12:23:35 +0200 Subject: [PATCH 1/4] Add CacheableVoterInterface into voters docs --- security/voters.rst | 90 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/security/voters.rst b/security/voters.rst index 01be3eb0745..25a14a68fd2 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -48,15 +48,91 @@ which makes creating a voter even easier:: abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool; } -.. _how-to-use-the-voter-in-a-controller: -.. tip:: +The Cacheable Voter Interface +----------------------------- +When Symfony calls the isGranted() method during runtime, it checks each voter +until it meets the access decision strategy set by the configuration. +While this generally works well, it can slow down performance in some cases. + +Imagine a backend displaying 20 items, each with 6 properties and 3 actions +(like edit, show, delete). Checking permissions for all these requires 180 calls +to each voter. With 5 voters, that's 900 calls. + +Usually, voters only need to focus on a specific permission (e.g., EDIT_BLOG_POST) +or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 introduces a +:class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface` + + namespace Symfony\Component\Security\Core\Authorization\Voter; + + interface CacheableVoterInterface extends VoterInterface + { + public function supportsAttribute(string $attribute): bool; + + // $subjectType is the value returned by `get_class($subject)` or `get_debug_type($subject)` + public function supportsType(string $subjectType): bool; + } + +If your voter returns false for these methods, it will cache this result, +and your voter won't be called again for that attribute or type. + +If you extend from the abstract Voter class, you don't need to implement +the interface directly—just override supportsAttribute() and/or supportsType(). + +For instance, if your voter handles multiple object types but all permissions +are prefixed with APPROVE_, do this: + + namespace App\Security; + + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class MyVoter extends Voter + { + public function supportsAttribute(string $attribute): bool + { + return str_starts_with($attribute, 'APPROVE_'); + } + + // ... + } + +If your voter handles various permissions for a specific type, do this: - Checking each voter several times can be time consuming for applications - that perform a lot of permission checks. To improve performance in those cases, - you can make your voters implement the :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface`. - This allows the access decision manager to remember the attribute and type - of subject supported by the voter, to only call the needed voters each time. + namespace App\Security; + + use App\Entity\BlogPost; + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class MyVoter extends Voter + { + public function supportsType(string $subjectType): bool + { + // you can't use a simple BlogPost::class === $subjectType comparison + // here because the given subject type could be the proxy class used + // by Doctrine when creating the entity object + return is_a($subjectType, BlogPost::class, true); + } + + // ... + } + +You can also combine the logic and avoid code repetition by implementing the `supports()` function like this: + + namespace App\Security; + + use Symfony\Component\Security\Core\Authorization\Voter\Voter; + + class MyVoter extends Voter + { + public function supports(string $attribute, mixed $subject): bool + { + $this->supportsAttribute($attribute) && $this->supportsType(get_debug_type($subject)); + } + + // ... + } + +.. _how-to-use-the-voter-in-a-controller: Setup: Checking for Access in a Controller ------------------------------------------ From 8b0d4a6ae77c1a5433bbd736764e00bb7ccb617c Mon Sep 17 00:00:00 2001 From: michnovka Date: Tue, 1 Oct 2024 12:29:37 +0200 Subject: [PATCH 2/4] Fixes for lint --- security/voters.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/security/voters.rst b/security/voters.rst index 25a14a68fd2..26b9be54557 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -51,16 +51,16 @@ which makes creating a voter even easier:: The Cacheable Voter Interface ----------------------------- -When Symfony calls the isGranted() method during runtime, it checks each voter -until it meets the access decision strategy set by the configuration. +When Symfony calls the isGranted() method during runtime, it checks each voter +until it meets the access decision strategy set by the configuration. While this generally works well, it can slow down performance in some cases. -Imagine a backend displaying 20 items, each with 6 properties and 3 actions -(like edit, show, delete). Checking permissions for all these requires 180 calls +Imagine a backend displaying 20 items, each with 6 properties and 3 actions +(like edit, show, delete). Checking permissions for all these requires 180 calls to each voter. With 5 voters, that's 900 calls. -Usually, voters only need to focus on a specific permission (e.g., EDIT_BLOG_POST) -or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 introduces a +Usually, voters only need to focus on a specific permission (e.g., EDIT_BLOG_POST) +or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 introduces a :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface` namespace Symfony\Component\Security\Core\Authorization\Voter; @@ -73,14 +73,14 @@ or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 i public function supportsType(string $subjectType): bool; } -If your voter returns false for these methods, it will cache this result, +If your voter returns false for these methods, it will cache this result, and your voter won't be called again for that attribute or type. -If you extend from the abstract Voter class, you don't need to implement +If you extend from the abstract Voter class, you don't need to implement the interface directly—just override supportsAttribute() and/or supportsType(). -For instance, if your voter handles multiple object types but all permissions -are prefixed with APPROVE_, do this: +For instance, if your voter handles multiple object types but all permissions +are prefixed with `APPROVE_`, do this: namespace App\Security; From 98cfdb53ba7d302ed87293f4fbbab858da8245f5 Mon Sep 17 00:00:00 2001 From: michnovka Date: Tue, 1 Oct 2024 12:32:32 +0200 Subject: [PATCH 3/4] Remove unnecessary section --- security/voters.rst | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/security/voters.rst b/security/voters.rst index 26b9be54557..22ef024ddbd 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -116,22 +116,6 @@ If your voter handles various permissions for a specific type, do this: // ... } -You can also combine the logic and avoid code repetition by implementing the `supports()` function like this: - - namespace App\Security; - - use Symfony\Component\Security\Core\Authorization\Voter\Voter; - - class MyVoter extends Voter - { - public function supports(string $attribute, mixed $subject): bool - { - $this->supportsAttribute($attribute) && $this->supportsType(get_debug_type($subject)); - } - - // ... - } - .. _how-to-use-the-voter-in-a-controller: Setup: Checking for Access in a Controller From 08ec6c3914aeff58e194b9cfea54d83939b9eb57 Mon Sep 17 00:00:00 2001 From: michnovka Date: Tue, 1 Oct 2024 12:35:47 +0200 Subject: [PATCH 4/4] Fix escaping --- security/voters.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/security/voters.rst b/security/voters.rst index 22ef024ddbd..c4ba988f9b1 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -51,7 +51,7 @@ which makes creating a voter even easier:: The Cacheable Voter Interface ----------------------------- -When Symfony calls the isGranted() method during runtime, it checks each voter +When Symfony calls the ``isGranted()`` method during runtime, it checks each voter until it meets the access decision strategy set by the configuration. While this generally works well, it can slow down performance in some cases. @@ -59,8 +59,8 @@ Imagine a backend displaying 20 items, each with 6 properties and 3 actions (like edit, show, delete). Checking permissions for all these requires 180 calls to each voter. With 5 voters, that's 900 calls. -Usually, voters only need to focus on a specific permission (e.g., EDIT_BLOG_POST) -or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 introduces a +Usually, voters only need to focus on a specific permission (e.g., ``EDIT_BLOG_POST``) +or object type (e.g., ``User``). This focus makes voters cacheable, so Symfony 5.4 introduces a :class:`Symfony\\Component\\Security\\Core\\Authorization\\Voter\\CacheableVoterInterface` namespace Symfony\Component\Security\Core\Authorization\Voter; @@ -69,7 +69,7 @@ or object type (e.g., User). This focus makes voters cacheable, so Symfony 5.4 i { public function supportsAttribute(string $attribute): bool; - // $subjectType is the value returned by `get_class($subject)` or `get_debug_type($subject)` + // $subjectType is the value returned by get_class($subject) or get_debug_type($subject) public function supportsType(string $subjectType): bool; } @@ -77,10 +77,10 @@ If your voter returns false for these methods, it will cache this result, and your voter won't be called again for that attribute or type. If you extend from the abstract Voter class, you don't need to implement -the interface directly—just override supportsAttribute() and/or supportsType(). +the interface directly - just override ``supportsAttribute()`` and/or ``supportsType()``. For instance, if your voter handles multiple object types but all permissions -are prefixed with `APPROVE_`, do this: +are prefixed with ``APPROVE_``, do this: namespace App\Security;