🎻

[Symfony] 任意のユーザーに対して isGranted() する方法

2023/04/19に公開

はじめに

Symfonyにおいて、ログインユーザーの権限を検証する際には isGranted() メソッドを使用します。

class Foo
{
    public function __construct(private Security $security)
    {
    }
    
    public function bar(): void
    {
        // ログインユーザーが 'ROLE_XXX' 権限を持っている場合のみ何かをする
        if ($this->security->isGranted('ROLE_XXX')) {
            // do something
        }
    }
}

ちなみに AbstractController を継承したコントローラ内では、Secuirity などをインジェクトしなくても $this->isGranted() というショートハンドが用意されています。

class FooController extends AbstractController
{
    public function barAction(): Response
    {
        // ログインユーザーが 'ROLE_XXX' 権限を持っている場合のみ何かをする
        if ($this->isGranted('ROLE_XXX')) {
            // do something
        }

        return new Response();
    }
}

しかし、これはあくまでログイン中のユーザー自身に対する検証にしか使えず、「任意のユーザーがある権限を持っているかどうか」を検証することはできません。

これを解決できる実装を考えてみます。

❌ 不完全な解決策

symfony security isgranted specific user とかでググると、

symfony - Is granted for other user - Stack Overflow

こんな感じの解決策が見つかります。

RoleHierarchyInterface::getReachableRoleNames() を使って、「指定したユーザーが、指定したROLEを持っているかどうか」を検証しようというものですね。

例えば以下のようなサービスクラスを作っておくことで、便利に使えそうです。

namespace App\Security;

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

class SecurityManager
{
    public function __construct(private RoleHierarchyInterface $roleHierarchy)
    {
    }

    public function isGranted(UserInterface $user, string $role): bool
    {
        return in_array($role, $this->roleHierarchy->getReachableRoleNames($user->getRoles()), true);
    }
}
use App\Security\SecurityManager;

class Foo
{
    public function __construct(private SecurityManager $sm)
    {
    }
    
    public function bar(UserInterface $user): void
    {
        // 指定されたユーザーが 'ROLE_XXX' 権限を持っている場合のみ何かをする
        if ($this->sm->isGranted($user, 'ROLE_XXX')) {
            // do something
        }
    }
}

しかし、symfony - Is granted for other user - Stack Overflow の回答にも注意書きされているとおり、この方法は Security Voter による検証に対応できていません。

つまり、「任意のユーザーがある対象物に対して特定の権限を持っているか」を検証することができず、対応としては不完全です。

⭕ 完全な解決策

もとの isGranted() メソッドの実装を見てみると、最終的には AuthorizationCheckerInterface にバインドされている security.authorization_checker というサービスの isGranted() を呼んでいる ことが分かります。

つまり、完全な対応をするには、指定のユーザーが実際にログイン状態にあるような AuthorizationCheckerInterface の実体を用意して、それに対して isGranted() を呼んであげれば よさそうです。

具体的には、以下のようなコードで実現可能です。

なぜこういうコードになるかは、Symfonyのコードを読んでみてください🙏

namespace App\Security;

use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;

class SecurityManager
{
    public function __construct(private AccessDecisionManagerInterface $adm)
    {
    }

    public function isGranted(UserInterface $user, mixed $attribute, mixed $subject = null): bool
    {
        $tokenStorage = new TokenStorage();
        $token = new PostAuthenticationToken($user, 'main', $user->getRoles());
        $tokenStorage->setToken($token);
        $authorizationChecker = new AuthorizationChecker($tokenStorage, $this->adm);

        return $authorizationChecker->isGranted($attribute, $subject);
    }
}

これで、標準の isGranted() とまったく同等の検証を、任意のユーザーに対して行うことができるようになりました。

use App\Security\SecurityManager;

class Foo
{
    public function __construct(private SecurityManager $sm)
    {
    }
    
    public function bar(UserInterface $user, Baz $baz): void
    {
        // 指定されたユーザーが $baz に対して 'EDIT' 権限を持っている場合のみ何かをする
        if ($this->sm->isGranted($user, 'EDIT', $baz)) {
            // do something
        }
    }
}

めでたしめでたし🍣

GitHubで編集を提案

Discussion