Использование классов избирателей для проверки прав доступа в Symfony 2

В Symfony права доступа к данным можно проверять используя ACL модуль, но чаще всего его использование является излишним,загромаждающим приложение. Куда удобнее использовать свои созданные правила, больше походящие на простые условия.

Эти правила могут применяться в различных случаях, например, ограничение доступа к приложению для целого ряда IP адресов: Как создать правила для блокировки IP адресов.

Каким образом Symfony использует Voter

Для того, чтобы использовать классы избирателей (Voters), сначала стоит понять как Symfony с ними взаимодействует. Все они просматриваются каждый раз при вызове метода isGranted() службы security.context. Каждое правило либо разрешает, либо запрещает доступ к определенному ресурсу.

В целом, Symfony использует три базовых подхода в качестве реакции на ответ: подтверждение, согласование и единогласие.

Более подробную информацию вы найдете в разделе о процессе принятия решение об отказе или разрешении доступа.

Интерфейс избирателя

Созданный вами класс избирателя должнен соответствовать интерфейсу VoterInterface:

<?php
interface VoterInterface
{
    public function supportsAttribute($attribute);
    public function supportsClass($class);
    public function vote(TokenInterface $token, $object, array $attributes);
}

Метод supportsAttribute() показывает поддерживает ли правило данный атрибут пользователя (роли, например, ROLE_USER или ACL EDIT и т.д.).

Метод supportsClass() показывает поддерживает ли правило класс объекта, к которому обращается пользователь.

Метод vote() отвечает за логику проверки прав доступа. Он должен вернуть одно из следующих значений:

  • VoterInterface::ACCESS_GRANTED: доступ разрешен
  • VoterInterface::ACCESS_ABSTAIN: правило не применимо и не может вернуть однозначный ответ
  • VoterInterface::ACCESS_DENIED: в доступе отказано

В следующем примере будут проверенны права доступа, основываясь на правилах, установленных разработчиком (пользователь должен быть владельцем объекта). Если условие не будет выполнено, то возвращаем VoterInterface::ACCESS_DENIED, в противном случае возвращаем VoterInterface::ACCESS_GRANTED. Если же решение о разрешении доступа не может быть принято этим правилом, то возвращаем VoterInterface::ACCESS_ABSTAIN.

Создание класса избирателя со своими правилами

Цель примера – создание правила, которое будет проверять имеет ли пользователь права на просмотр и редактирование определенного объекта. Пример выполнения:

<?php
// src/Acme/DemoBundle/Security/Authorization/Voter/PostVoter.php
namespace Acme\DemoBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class PostVoter implements VoterInterface
{
    const VIEW = 'view';
    const EDIT = 'edit';
    public function supportsAttribute($attribute)
    {
        return in_array($attribute, array(
            self::VIEW,
            self::EDIT,
        ));
    }
    public function supportsClass($class)
    {
        $supportedClass = 'Acme\DemoBundle\Entity\Post';
        return $supportedClass === $class || is_subclass_of($class, $supportedClass);
    }
    /**
     * @var \Acme\DemoBundle\Entity\Post $post
     */
    public function vote(TokenInterface $token, $post, array $attributes)
    {
        // проверяет подерживается ли класс текущего объекта избирателем
        if (!$this->supportsClass(get_class($post))) {
            return VoterInterface::ACCESS_ABSTAIN;
        }
        // проверка на корректность использования, позволен только один атрибут
        // но это необязательно, просто для наглядности приведна  
        // проверка одного атрибута
        if (1 !== count($attributes)) {
            throw new \InvalidArgumentException(
                'Only one attribute is allowed for VIEW or EDIT'
            );
        }
        // set the attribute to check against
        $attribute = $attributes[0];
        // проверить, если данный атрибут охватывается этим избирателем
        if (!$this->supportsAttribute($attribute)) {
            return VoterInterface::ACCESS_ABSTAIN;
        }
        // получить текущего залогиненного пользователя
        $user = $token->getUser();
        // убедитесь, что объект яляется пользователем (т.е., что пользователь вошел в систему)
        if (!$user instanceof UserInterface) {
            return VoterInterface::ACCESS_DENIED;
        }
        switch($attribute) {
            case self::VIEW:
                // Объект данных может иметь, например, метод isPrivate()
                // который проверяет логический атрибут $private
                if (!$post->isPrivate()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;
            case self::EDIT:
                // мы предполагаем, что наша объект данных имеет метод getOwner() to
                // для получения текущего владельца (пользователя) для этого объекта данных
                if ($user->getId() === $post->getOwner()->getId()) {
                    return VoterInterface::ACCESS_GRANTED;
                }
                break;
        }
        return VoterInterface::ACCESS_DENIED;
    }
}

Вот и все! Правило создано. Осталось внедрить его в систему безопасности приложения.

Объявление класса в качестве службы

Чтобы внедрить ваш класс избирателя в систему безопасности приложения, его надо объявить в качестве службы и поставить тег security.voter.

# src/Acme/DemoBundle/Resources/config/services.yml
services:
    security.access.post_voter:
        class:      Acme\DemoBundle\Security\Authorization\Voter\PostVoter
        public:     false
        tags:
            - { name: security.voter }

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

Созданное вами правила будут всегда проверяться при вызове методе isGranted() из службы security.context.

<?php
// src/Acme/DemoBundle/Controller/PostController.php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class PostController extends Controller
{
    public function showAction($id)
    {
        // получаем инстанс Post
        $post = ...;
        // имейте в виду это будет вызывать все зарегистрированные избиратели
        if (false === $this->get('security.context')->isGranted('view', $post)) {
            throw new AccessDeniedException('Unauthorised access!');
        }
        return new Response('<h1>'.$post->getName().'</h1>');
    }
}

Как видите, ничего сложного!