Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Права на конкретную запись

Роли отвечают на вопрос «кто этот пользователь». Но часто нужно другое: может ли он изменить именно эту статью? Такие правила выносят в избиратель (voter) — класс, который по действию и объекту отвечает «да» или «нет».

Создание

php bin/console make:voter PostVoter
// src/Security/Voter/PostVoter.php
namespace App\Security\Voter;
 
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
{
    public const EDIT = 'POST_EDIT';
    public const DELETE = 'POST_DELETE';
 
    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE], true)
            && $subject instanceof Post;
    }
 
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
 
        if (!$user instanceof User) {
            return false;
        }
 
        return $subject->getAuthor() === $user;
    }
}

supports() решает, относится ли вопрос к этому избирателю, voteOnAttribute() даёт ответ. Гость получает отказ сразу.

Проверка в контроллере

#[Route('/blog/{id}/edit', name: 'blog_edit')]
public function edit(Post $post): Response
{
    $this->denyAccessUnlessGranted(PostVoter::EDIT, $post);
 
    // …
}

Если избиратель ответил «нет», пользователь получит ошибку 403, а гость отправится на страницу входа.

Проверка атрибутом

use Symfony\Component\Security\Http\Attribute\IsGranted;
 
#[Route('/blog/{id}/delete', name: 'blog_delete', methods: ['POST'])]
#[IsGranted(PostVoter::DELETE, subject: 'post')]
public function delete(Post $post, EntityManagerInterface $em): Response
{
    $em->remove($post);
    $em->flush();
 
    return $this->redirectToRoute('blog_index');
}

subject — имя аргумента метода, который передаётся избирателю.

Проверка в шаблоне

{% if is_granted('POST_EDIT', post) %}
    <a href="{{ path('blog_edit', {id: post.id}) }}">Редактировать</a>
{% endif %}

Разные правила для разных действий

protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
    $user = $token->getUser();
 
    if (!$user instanceof User) {
        return false;
    }
 
    return match ($attribute) {
        self::EDIT => $subject->getAuthor() === $user,
        self::DELETE => $subject->getAuthor() === $user && !$subject->isPublished(),
        default => false,
    };
}

Администратору можно всё

Избиратель — сервис и может запрашивать зависимости. Роли проверяет сервис Security:

use Symfony\Bundle\SecurityBundle\Security;
 
public function __construct(private Security $security) {}
 
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
    if ($this->security->isGranted('ROLE_ADMIN')) {
        return true;
    }
 
    // обычные правила
}

Частая ошибка

Проверки if ($post->getAuthor() !== $this->getUser()), разбросанные по контроллерам, легко забыть в одном из методов — и чужую статью сможет изменить любой. Правило должно жить в одном месте, в избирателе, а контроллеры и шаблоны только спрашивают is_granted.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.