Я перенес свои сообщения в собственный блог, потому что Medium становится все менее и менее удобным для читателей (платный доступ, невозможность выделить код и т. Д.). Чтобы прочитать эту статью в более приятном и дружественном контексте, прочтите ее в моем личном блоге и подписывайтесь на меня в Twitter, чтобы получать уведомления!

Https://titouangalopin.com/using-symfony-security-voters-to-check-user-permissions-with-ease/

Проверка разрешений пользователей - важная часть многих веб-проектов. Одна ошибка может иметь разрушительные последствия, приводя к серьезным утечкам данных или пагубным последствиям.

Вот почему возможность легко создавать, поддерживать и тестировать разрешения вашего веб-приложения чрезвычайно важна: это должно быть как можно проще из-за того, насколько это важно. Более того, тесты, охватывающие разрешения и безопасность вашего приложения, должны охватывать 100% возможных случаев: один пропущенный случай может привести к серьезным проблемам.

Здесь может помочь такой фреймворк, как Symfony. В Symfony пользовательская система и связанные с ней разрешения обрабатываются компонентом безопасности, который создает расширяемый контекст для ваших проверок.

Компонент безопасности

Компонент Symfony Security содержит множество функций, и я не буду здесь вдаваться в подробности. Однако важно понимать глобальную архитектуру этого компонента:

Когда компонент обрабатывает запрос, есть два основных шага:

  • аутентификация пытается найти текущего пользователя по запросу (используя токен JWT, файл cookie сеанса PHP, комбинацию имени пользователя и пароля и т. д.).
  • авторизация проверяет, разрешен ли текущий пользователь доступ к запрошенному ресурсу (и возвращает 403, если нет).

Я хотел бы рассказать вам, как полностью настроить этап авторизации с помощью избирателей.

Избиратели Symfony Security

Голосующие за безопасность - это способ для компонента безопасности делегировать проверку разрешений вашему приложению. Используя голосование, ваше приложение сможет обрабатывать настраиваемые действия над настраиваемыми объектами с настраиваемой логикой.

По сути, идея избирателя состоит в том, чтобы разрешить следующее:

namespace App\Controller;

class ProjectController
{
    public function edit(Project $project)
    {
        $this->denyAccessUnlessGranted('edit', $project);
    }
}

В этих нескольких строках кода компонент Security будет вызывать всех объявленных избирателей, проверяя, поддерживают ли они данный объект ($ project) для этого конкретного атрибута (edit). Если они его поддерживают, компонент спросит их, разрешено ли пользователю редактировать проект.

Вы можете объявить избирателей, создав простой класс, реализующий компонент безопасности VoterInterface. Однако гораздо проще расширить класс Voter напрямую:

namespace App\Security;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ProjectVoter extends Voter
{
    public const EDIT = 'edit';

    private const ATTRIBUTES = [
        self::EDIT,
    ];

    protected function supports($attribute, $subject)
    {
        return $subject instanceof Project 
               && in_array($attribute, self::ATTRIBUTES);
    }

    /**
     * @param string $a
     * @param Project $p
     * @param TokenInterface $t
     *
     * @return bool
     */
    protected function voteOnAttribute(
        $attribute, 
        $project, 
        TokenInterface $token
    ) {
        switch ($attribute) {
            case self::EDIT:
                return $this->isOwner($token->getUser(), $project);
        }

        throw new \LogicException('Invalid attribute: '.$attribute);
    }

    private function isOwner(?User $user, Project $project)
    {
        if (!$user) {
            return false;
        }

        return $user->getId() === $project->getOwner()->getId();
    }
}

Этот избиратель объявляет, как обрабатывать атрибут «редактировать» для объекта Project. Точнее, он голосует за атрибут: он дает компоненту безопасности информацию о том, должен ли этот объект быть доступен для данного пользователя с данным действием. Если метод voteOnAttribute возвращает false, сущность недоступна для текущего пользователя. В противном случае это так.

На мой взгляд, избиратели - одна из наиболее хорошо спроектированных частей Symfony: они являются мощной точкой расширения фреймворка, их легко понять и их легко можно протестировать на единицу.

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

namespace App\Tests\Security;

class ProjectVoterTest extends TestCase
{
    private function createUser(int $id): User
    {
        $user = $this->createMock(User::class);
        $user->method('getId')->willReturn($id);

        return $user;
    }

    public function provideCases()
    {
        yield 'anonymous cannot edit' => [
            'edit',
            new Project($this->createUser(1)),
            null,
            Voter::ACCESS_DENIED
        ];

        yield 'non-owner cannot edit' => [
            'edit',
            new Project($this->createUser(1)),
            $this->createUser(2),
            Voter::ACCESS_DENIED
        ];

        yield 'owner can edit' => [
            'edit',
            new Project($this->createUser(1)),
            $this->createUser(1),
            Voter::ACCESS_GRANTED
        ];
    }

    /**
     * @dataProvider provideCases
     */
    public function testVote(
        string $attribute,
        Project $project,
        ?User $user,
        $expectedVote
    ) {
        $voter = new ProjectVoter();

        $token = new AnonymousToken('secret', 'anonymous');
        if ($user) {
            $token = new UsernamePasswordToken(
                $user, 'credentials', 'memory'
            );
        }

        $this->assertSame(
            $expectedVote,
            $voter->vote($token, $project, [$attribute])
        );
    }
}

Этот модульный тест можно легко расширить: добавив больше случаев, вы можете охватить все возможные сценарии для каждой роли, действия и объекта вашего приложения. Это позволяет гарантировать, что ваш избиратель правильно рассмотрит все дела, и при этом будет работать очень быстро, так как это модульное тестирование!