[Symfony] 任意のユーザーに対して isGranted() する方法
はじめに
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
}
}
}
めでたしめでたし🍣
Discussion