Как использовать списки контроля доступа (СКД)

Как использовать списки контроля доступа (СКД)

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

Использование СКД нетривиально, и для более простых случаев оно может быть излишним. Если ваша логика доступа может быть описана простым кодом (например, проверить, владеет ли текущий Пользователь Блогом), то рассмотрите вариант использования избирателя. Избирателю передаётся объект, по поводу которого проходит голосование, и вы можете использовать его для принятия сложных решений и эффективной реализации ваших собственных СКД. Принуждение к авторизации (например, часть isGranted()) будет выглядеть схоже с тем, что вы видите в этой записи, но за кулисами, с логикой будет работать ваш класс избирателя, а не система СКД.

Представьте, что вы разрабатываете систему блога, где ваши пользователи могут комментировать ваши записи. Теперь, вы хотите, чтобы пользователь мог редактировать собственные комментарии, но не комментарии других пользователей; кроме того, вы хотите, чтобы вы могли редактировать все комментарии. В этом случае, Comment будет объектом домена, к которому вы хотите ограничить доступ. Вы можете использовать несколько подходов для того, чтобы это достичь, используя Symfony, два базовых подхода (не исчерпывающих) это:

  • Форсирование безопасности в ваших бизнес-методах: По существу, это означает, что внутри каждого Comment будет ссылка на всех пользователей, у которых есть доступ, а потом эти пользователи будут сравниваться с предоставленным Token.
  • Форсирование безопасности с ролями: В этом подходе вы будете добавлять роль для каждого объекта Comment, т.е. ROLE_COMMENT_1, ROLE_COMMENT_2, и т.д.

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

К счастью, существует способ лучше, о котором вы сейчас узнаете.

Самонастройка

Теперь, перед тем, как вы наконец займетёсь делом, вам нужно проделать некоторую самонастройку. Для начала, вам нужно сконфигурировать связь, которую должна использовать система СКД:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        # ...
    
        acl:
            connection: default
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <acl connection="default" />
        </config>
    </srv:container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'acl' => array(
            'connection' => 'default',
        ),
    ));
    

Note

Система СКД требует связи либо из Doctrine DBAL (используемой по умолчанию), либо из Doctrine MongoDB (используемой с MongoDBAclBundle). Однако, это не означает, что вам нужно использовать Doctrine ORM или ODM для отображения ваших объектов домена. Вы можете использовать любую систему отображения, которую вы хотите, будь это Doctrine ORM, MongoDB ODM, Propel, чистый SQL, и т.д. Выбор за вами.

После того, как соединение сконфигурировано, вам нужно импортировать структуру БД, выполнив следующую команду:

1
$ php bin/console init:acl

Начало работы

Возвращаясь к маленькому примеру в начале, вы теперь можете реализовать для него СКД.

Когда СКД будет создан, вы можете гарантировать доступ к объектам, создав Запись контроля доступа (ЗКД), чтобы закрепить отношения между сущностью и вашим пользователем.

Создание СКД и добавление ЗКД

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/AppBundle/Controller/BlogController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\UserSecurityIdentity;
use Symfony\Component\Security\Acl\Permission\MaskBuilder;

class BlogController extends Controller
{
    // ...

    public function addCommentAction(Post $post)
    {
        $comment = new Comment();

        // ... установить $form, и отправить данные

        if ($form->isSubmitted() && $form->isValid()) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->persist($comment);
            $entityManager->flush();

            // создание СКД
            $aclProvider = $this->get('security.acl.provider');
            $objectIdentity = ObjectIdentity::fromDomainObject($comment);
            $acl = $aclProvider->createAcl($objectIdentity);

            // получение личности безопасности текущего пользователя, находящегося в системе
            $tokenStorage = $this->get('security.token_storage');
            $user = $tokenStorage->getToken()->getUser();
            $securityIdentity = UserSecurityIdentity::fromAccount($user);

            // гарантия доступа владельца
            $acl->insertObjectAce($securityIdentity, MaskBuilder::MASK_OWNER);
            $aclProvider->updateAcl($acl);
        }
    }
}

В этом отрезке кода находятся несколько важных решений реализации. Сейчас, я хочу выделить только два:

Во-первых, вы могли заметить, что ->createAcl() не принимает объекты домена напрямую, а принимает только реализации ObjectIdentityInterface. Этот дополнительный обходной шаг позволяет вам работать с СКД даже когда у вас нет экземпляра объекта домена на руках. Это будет крайне полезно, если вы хотите проверить разрешения на предмет большого количества объектов, не насыщая эти объекты.

Вторая интересная часть - это вызов ->insertObjectAce(). В примере, вы гарантируете пользователю, который находится в системе, доступ к Комментарию. MaskBuilder::MASK_OWNER - это предпопределённая целочисленная битовая маска; не волнуйтесь, разработчик масок извлечёт большинство технических деталей, но используя эту технику, вы можете хранить много разных разрешений в одной строке БД, что даёт ощутимое преимущество в производительности.

Tip

Порядок, в котором проверяются ЗКД очень важен. Основное правило: вы должны размещать более конкретные записи в начале.

Проверка доступа

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Controller/BlogController.php

// ...

class BlogController
{
    // ...

    public function editCommentAction(Comment $comment)
    {
        $authorizationChecker = $this->get('security.authorization_checker');

        // проверка доступа к редактированию
        if (false === $authorizationChecker->isGranted('EDIT', $comment)) {
            throw new AccessDeniedException();
        }

        // ... получить объект комментария и выполнить вашу редактуру здесь
    }
}

В этом примере, вы проверяете, имеет ли пользователь разрешение EDIT. Внутренне, Symfony связывает разрешение с несколькими целочисленными битовыми масками, и проверяет, имеет ли пользователь какие-либо из них.

Note

Вы можете определить до 32 базовых разрешений (в зависимости от вашей ОС, PHP может варьироватся между 30 и 32). В дополнение, вы также можете определить кумулятивные разрешения.

Кумулятивные разрешения

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

MaskBuilder может быть легко использован для создания битовых масок, путём комбинирования нескольких базовых разрешений:

1
2
3
4
5
6
7
8
$builder = new MaskBuilder();
$builder
    ->add('view')
    ->add('edit')
    ->add('delete')
    ->add('undelete')
;
$mask = $builder->get(); // int(29)

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

1
2
$identity = new UserSecurityIdentity('johannes', 'AppBundle\Entity\User');
$acl->insertObjectAce($identity, $mask);

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

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.