How to Use Voters to Check User Permissions

Voters are Symfony's most powerful way of managing permissions. They allow you to centralize all permission logic, then reuse them in many places.

投票者は、パーミッションを管理する Symfony の最も強力な方法です。すべての権限ロジックを一元化し、多くの場所で再利用できます。

However, if you don't reuse permissions or your rules are basic, you can always put that logic directly into your controller instead. Here's an example how this could look like, if you want to make a route accessible to the "owner" only:

ただし、アクセス許可を再利用しない場合、またはルールが基本的なものである場合は、代わりに常にそのロジックをコントローラーに直接配置できます。 「所有者」のみがルートにアクセスできるようにしたい場合、これがどのように見えるかの例を次に示します。
1
2
3
4
5
6
7
// src/Controller/PostController.php
// ...

// inside your controller action
if ($post->getOwner() !== $this->getUser()) {
    throw $this->createAccessDeniedException();
}

In that sense, the following example used throughout this page is a minimal example for voters.

その意味で、このページ全体で使用されている次の例は、有権者にとって最小限の例です。

Here's how Symfony works with voters: All voters are called each time you use the isGranted() method on Symfony's authorization checker or call denyAccessUnlessGranted() in a controller (which uses the authorization checker), or by access controls.

Symfony がボーターを処理する方法は次のとおりです: すべてのボーターは、Symfony の承認チェッカーで isGranted() メソッドを使用するか、コントローラー (承認チェッカーを使用する) で denyAccessUnlessGranted() を使用するか、アクセス制御によって呼び出されるたびに呼び出されます。

Ultimately, Symfony takes the responses from all voters and makes the final decision (to allow or deny access to the resource) according to the strategy defined in the application, which can be: affirmative, consensus, unanimous or priority.

最終的に、Symfony はすべての有権者からの応答を受け取り、アプリケーションで定義された戦略に従って (リソースへのアクセスを許可または拒否する) 最終決定を下します。

The Voter Interface

A custom voter needs to implement VoterInterface or extend Voter, which makes creating a voter even easier:

カスタム ボーターは、VoterInterface を実装するか、Voter を拡張する必要があります。これにより、ボーターの作成がさらに簡単になります。
1
2
3
4
5
6
7
8
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

abstract class Voter implements VoterInterface
{
    abstract protected function supports(string $attribute, mixed $subject): bool;
    abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool;
}

Tip

ヒント

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 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.

多くの権限チェックを実行するアプリケーションでは、各投票者を数回チェックすると時間がかかる場合があります。このような場合のパフォーマンスを向上させるために、投票者に CacheableVoterInterface を実装させることができます。これにより、アクセス決定マネージャーは、投票者がサポートするサブジェクトの属性とタイプを記憶し、必要な投票者のみを毎回呼び出すことができます。

Setup: Checking for Access in a Controller

Suppose you have a Post object and you need to decide whether or not the current user can edit or view the object. In your controller, you'll check access with code like this:

Post オブジェクトがあり、現在のユーザーがオブジェクトを編集または表示できるかどうかを決定する必要があるとします。コントローラーで、次のようなコードでアクセスを確認します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/Controller/PostController.php

// ...
class PostController extends AbstractController
{
    #[Route('/posts/{id}', name: 'post_show')]
    public function show($id): Response
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "view" access: calls all voters
        $this->denyAccessUnlessGranted('view', $post);

        // ...
    }

    #[Route('/posts/{id}/edit', name: 'post_edit')]
    public function edit($id): Response
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "edit" access: calls all voters
        $this->denyAccessUnlessGranted('edit', $post);

        // ...
    }
}

The denyAccessUnlessGranted() method (and also the isGranted() method) calls out to the "voter" system. Right now, no voters will vote on whether or not the user can "view" or "edit" a Post. But you can create your own voter that decides this using whatever logic you want.

denyAccessUnlessGranted() メソッド (および isGranted() メソッド) は、「投票者」システムを呼び出します。現在、ユーザーが投稿を「表示」または「編集」できるかどうかについて投票する有権者はいません。ただし、必要なロジックを使用してこれを決定する独自の有権者を作成できます。

Creating the custom Voter

Suppose the logic to decide if a user can "view" or "edit" a Post object is pretty complex. For example, a User can always edit or view a Post they created. And if a Post is marked as "public", anyone can view it. A voter for this situation would look like this:

ユーザーが Post オブジェクトを「表示」または「編集」できるかどうかを決定するロジックが非常に複雑であるとします。たとえば、ユーザーは自分が作成した投稿をいつでも編集または表示できます。また、投稿が「公開」としてマークされている場合、誰でもそれを表示できます。この状況の有権者は次のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // these strings are just invented: you can use anything
    const VIEW = 'view';
    const EDIT = 'edit';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, [self::VIEW, self::EDIT])) {
            return false;
        }

        // only vote on `Post` objects
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        // you know $subject is a Post object, thanks to `supports()`
        /** @var Post $post */
        $post = $subject;

        return match($attribute) {
            self::VIEW => $this->canView($post, $user),
            self::EDIT => $this->canEdit($post, $user),
            default => throw new \LogicException('This code should not be reached!')
        };
    }

    private function canView(Post $post, User $user): bool
    {
        // if they can edit, they can view
        if ($this->canEdit($post, $user)) {
            return true;
        }

        // the Post object could have, for example, a method `isPrivate()`
        return !$post->isPrivate();
    }

    private function canEdit(Post $post, User $user): bool
    {
        // this assumes that the Post object has a `getOwner()` method
        return $user === $post->getOwner();
    }
}

That's it! The voter is done! Next, configure it.

それでおしまい!投票完了!次に、構成します。

To recap, here's what's expected from the two abstract methods:

要約すると、2 つの抽象メソッドから期待されることは次のとおりです。
Voter::supports(string $attribute, mixed $subject)
When isGranted() (or denyAccessUnlessGranted()) is called, the first argument is passed here as $attribute (e.g. ROLE_USER, edit) and the second argument (if any) is passed as $subject (e.g. null, a Post object). Your job is to determine if your voter should vote on the attribute/subject combination. If you return true, voteOnAttribute() will be called. Otherwise, your voter is done: some other voter should process this. In this example, you return true if the attribute is view or edit and if the object is a Post instance.
isGranted() (または denyAccessUnlessGranted()) が呼び出されると、最初の引数はここで $attribute (ROLE_USER、edit など) として渡され、2 番目の引数 (存在する場合) は $subject (null、Postobject など) として渡されます。あなたの仕事は、有権者が属性/件名の組み合わせに投票する必要があるかどうかを判断することです。 true を返すと、voteOnAttribute() が呼び出されます。それ以外の場合、あなたの有権者は終了です。他の有権者がこれを処理する必要があります。この例では、属性が view または edit で、オブジェクトが Post インスタンスの場合に true を返します。
voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token)
If you return true from supports(), then this method is called. Your job is to return true to allow access and false to deny access. The $token can be used to find the current user object (if any). In this example, all of the complex business logic is included to determine access.
supports() から true を返すと、このメソッドが呼び出されます。アクセスを許可するには true を返し、アクセスを拒否するには false を返します。$token を使用して、現在のユーザー オブジェクト (存在する場合) を見つけることができます。この例では、複雑なビジネス ロジックのすべてが含まれており、アクセスを決定しています。

Configuring the Voter

To inject the voter into the security layer, you must declare it as a service and tag it with security.voter. But if you're using the default services.yaml configuration, that's done automatically for you! When you call isGranted() with view/edit and pass a Post object, your voter will be called and you can control access.

投票者をセキュリティ層に挿入するには、それをサービスとして宣言し、security.voter でタグ付けする必要があります。ただし、デフォルトの services.yaml 構成を使用している場合、これは自動的に行われます。 view/edit で isGranted() を呼び出して Post オブジェクトを渡すと、voter が呼び出され、アクセスを制御できます。

Checking for Roles inside a Voter

What if you want to call isGranted() from inside your voter - e.g. you want to see if the current user has ROLE_SUPER_ADMIN. That's possible by injecting the Security into your voter. You can use this to, for example, always allow access to a user with ROLE_SUPER_ADMIN:

投票者の内部から isGranted() を呼び出したい場合はどうなりますか?現在のユーザーが ROLE_SUPER_ADMIN を持っているかどうかを確認します。これは、投票者にセキュリティを注入することで可能になります。たとえば、これを使用して、ROLE_SUPER_ADMIN を持つユーザーへのアクセスを常に許可することができます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/Security/PostVoter.php

// ...
use Symfony\Bundle\SecurityBundle\Security;

class PostVoter extends Voter
{
    // ...

    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool
    {
        // ...

        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
            return true;
        }

        // ... all the normal voter logic
    }
}

If you're using the default services.yaml configuration, you're done! Symfony will automatically pass the security.helper service when instantiating your voter (thanks to autowiring).

デフォルトの services.yaml 構成を使用している場合は、これで完了です。 Symfony は、voter をインスタンス化するときに security.helperservice を自動的に渡します (オートワイヤーのおかげです)。

Changing the Access Decision Strategy

Normally, only one voter will vote at any given time (the rest will "abstain", which means they return false from supports()). But in theory, you could make multiple voters vote for one action and object. For instance, suppose you have one voter that checks if the user is a member of the site and a second one that checks if the user is older than 18.

通常、任意の時点で 1 人の有権者のみが投票します (残りの有権者は「棄権」します。つまり、supports() から false が返されます)。しかし、理論的には、複数の投票者に 1 つのアクションとオブジェクトに投票させることができます。たとえば、ユーザーがサイトのメンバーであるかどうかを確認する 1 人の投票者と、ユーザーが 18 歳以上かどうかを確認する 2 番目の投票者がいるとします。

To handle these cases, the access decision manager uses a "strategy" which you can configure. There are four strategies available:

これらのケースを処理するために、アクセス決定マネージャーは構成可能な「戦略」を使用します。利用可能な戦略は 4 つあります。
affirmative (default)
This grants access as soon as there is one voter granting access;
これにより、アクセスを許可する有権者が 1 人になるとすぐにアクセスが許可されます。
consensus
This grants access if there are more voters granting access than denying. In case of a tie the decision is based on the allow_if_equal_granted_denied config option (defaulting to true);
これにより、拒否よりもアクセスを許可する有権者が多い場合にアクセスが許可されます。同点の場合、決定は allow_if_equal_granted_denied 設定オプション (デフォルトは true) に基づきます。
unanimous
This only grants access if there is no voter denying access.
これは、アクセスを拒否する有権者がいない場合にのみアクセスを許可します。
priority
This grants or denies access by the first voter that does not abstain, based on their service priority;
これにより、サービスの優先度に基づいて、棄権しない最初の有権者によるアクセスが許可または拒否されます。

Regardless the chosen strategy, if all voters abstained from voting, the decision is based on the allow_if_all_abstain config option (which defaults to false).

選択した戦略に関係なく、すべての有権者が投票を棄権した場合、決定は allow_if_all_abstain 設定オプション (デフォルトは false) に基づいて行われます。

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:

上記のシナリオでは、ユーザーに投稿を読むためのアクセスを許可するために、両方の投票者がアクセスを許可する必要があります。この場合、デフォルトの戦略はもはや有効ではなく、代わりに全会一致を使用する必要があります。これは、セキュリティ構成で設定できます。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

Custom Access Decision Strategy

If none of the built-in strategies fits your use case, define the strategy_service option to use a custom service (your service must implement the AccessDecisionStrategyInterface):

ユース ケースに適合する組み込み戦略がない場合は、strategy_service オプションを定義してカスタム サービスを使用します (サービスは AccessDecisionStrategyInterface を実装する必要があります)。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy_service: App\Security\MyCustomAccessDecisionStrategy
        # ...

Custom Access Decision Manager

If you need to provide an entirely custom access decision manager, define the service option to use a custom service as the Access Decision Manager (your service must implement the AccessDecisionManagerInterface):

完全にカスタムのアクセス決定マネージャーを提供する必要がある場合は、サービス オプションを定義して、カスタム サービスをアクセス決定マネージャーとして使用します (サービスは AccessDecisionManagerInterface を実装する必要があります)。
  • YAML
    YAML
  • XML
    XML
  • PHP
    PHP
1
2
3
4
5
# config/packages/security.yaml
security:
    access_decision_manager:
        service: App\Security\MyCustomAccessDecisionManager
        # ...