Как стать автором
Обновить

Комментарии 17

Не совсем понимаю почему бы не использовать встроенный в Symfony 2 ACL.
Если честно, я просто не понял сути этого механизма. Насколько я понял из описания, он используется для контроля доступа к конкретному экземпляру объекта (комментарию пользователя в примере), а не к классу однотипных объектов (ко всем заказ-нарядам, как моем случае). Да и не очень понятно как это увязывать с БД. Если вы можете дать ссылки на какие-то другие статьи про ACL, буду очень благодарен.
Вы все поняли правильно. Меня смущает вот эта проверка: is_granted('[наименование привилегии]', [объект]), потому и предложил посмотреть в сторону ACL.
Если вам не нужна проверка на уровне объекта то почему бы не использовать просто массив ролей для пользователя, аля: [ROLE_CREATE_WORKORDER, ROLE_UPDATE_WORKORDER, ROLE_CREATE_TICKET, ROLE_DELETE_TICKET], или раз уж «Один пользователь имеет строго одну роль» то использовать группу к которой уже привязывать список ролей (у себя делаю именно так).
По сути тот метод который я предложил и есть ваш массив ролей, только со срезом по классу объекта с помощью voter и передачи экземпляра объекта в него. То есть пользователь имеет доступ CREATE_WORKORDER EDIT_WORKORDER VIEW_WORKORDER, но в базе и во всех вызовах эти права у меня именуются в виде create, view, edit для класса объекта workorder. Такие же права могут быть для другого объекта salesorder: create, view, edit. Тк мне показалось неудобным придумывать уникальный и к тому же длинный идентификатор для каждого нового разрешения, которое может потребовать бизнес-логика.
Спасибо за статью. Хотелось бы заметить, что get_class($object) в случае с doctrine 2 не всегда вернет корректное имя класса, лучше использовать \Doctrine\Common\Util\ClassUtils::getClass
Покопал немного поисковики, но ничего конкретного о причинах и условиях появления проблемы не нашел. В моем случае возвращаемые значения равны. Но думаю, разработчики doctrine не от скуки програмили этот класс. Спасибо за комментарий! PS: Какой любопытный здесь сайт. Рейтинги, кармы…
Недавно решал похожую задачу с Voter'ами для разграничения прав по тарифным планам. Отлично подходит этот механизм.
Единственное, может кто-то сталкивался — как вы работаете с isGranted(...) в CLI скриптах (symfony commands/PHPUnit tests)? Там ведь $token не определен.

Для PHPUnit тестов я инициализировал securityContext через new AnonymousToken(...) — но не знаю, павильно ли это.
Не могу представить ситуацию когда в Symfony Command понадобится isGranted, но вообще никто не мешает добавить опции --user и --pass и авторизовать пользователя.
Для модульных тестов просто делается мок, но если сервисы построены правильно обычно даже этого не требуется, т.к. вся аутентификация происходит уровнем выше: или в контроллере, или через Secure аннотацию сервиса.
Ну вот вам наш пример: имеются тарифные планы на сайте, проверка которых реализуется с помощью isGranted('%такой-то план%'). Внутри в Voter'е проверяется текущий юзер, его тарифный план. Мы вынесли всю сложную (времезатратную) логику в сервер очередей, при этом когда начинает работать Consumer, естественно уже никакаго юзера нет. Но при этом из множества действий необходимо для конкретного тарифного плана выполнять только определенные.

или через Secure аннотацию сервиса.

Не совсем здесь вас понял. Тоесть при наличии secure аннотации у сервиса (над классом?) он может вызваться или нет? Будет ли кидаться исключение, или код просто пропустит этот участок?
Примера не понял. Можно чуть подробней?
А про тесты, я обычно в config.yml для тестового окружения просто не помечаю сервисы тэгом security.secure_service. Подробнее можно почитать в документации. Хотя конечно все зависит от задач.
Видимо, плохо я пояснил. Постараюсь по-подробнее:

В системе имеются три тарифных плана для пользователей, которые дают им разные фичи. Чем дороже план, тем больше фич имеет пользователь. Проверка на то, доступен ли тот или иной функционал пользователю производится через isGranted('') и Voter.

Например, в контроллере:

// ...
if ($secureContext->isGranted('PRICE_PLAN_XXX')) {
    // делаем что-то специфическое
}
// ...


Внутри это работает через Voter, первый параметр которого содержит $token — через который мы получаем текущего сессионного пользователя и проверяем, действительно ли у него в БД содержится необходимый план и следует ли дать что-то (ну или запретить).

Вот, как я уже сказал, много функционала у нас вынесено в сервер очередей, чтобы как можно больше ускорить загрузку страницы. Так, например, генерация PDF — ее обрабатывает отдельный Consumer в бэкграунде. Условимся, что если пользователь имеет тарифный план YYY, который дороже чем XXX, то ему дополнительно шлем еще 1 файл. Так вот чтобы это проверить, мы в Consumer (та же коснольная команда, почти), пишем опять проверку:

// ...
if ($secureContext->isGranted('PRICE_PLAN_YYY')) {
    // шлем еще 1 файл
}
// ...


и тут возникает ошибка, что security context не инициализирован — нет токена. Выше я уже писал, что проблему временно решил через инициализацию анонимным токеном (встроенный в Symfony), и передачей объекта $user вторым параметром:

// ...
if ($secureContext->isGranted('PRICE_PLAN_YYY', $user)) {
    // шлем еще 1 файл
}
// ...


В поисках более лучшего решения…
Насколько я понял в текущей реализации Вы используете модель в качестве ресурса для ACL, почему бы не использовать контроллер, как ресурс? Это как раз логика работы приложения, а не логика работы домена. Тем более, что ранее Вы работали с ZF1, где это прописано во всех туториалах.

Типичный пример: 2 разных уровня доступа к модели(для одной роли на чтение, для другой — чтение + запись), в Вашем случае надо будет генерить новую модель со своим getResourceId().
Касаемо двух моделей в моей ситуации — мне такое сложно представить, так как контроль прав и все вызовы для БД проходят только через контроллер. Но вы правы. В качестве ресурса лучше использовать контроллер. Начал это понимать сегодня. Если кому-то будет интересно, то название контроллера и действия в twig можно получить так {{ app.request.attributes.get("_controller") }}
Почему бы тогда не использовать секцию access_control в security.yml? Что-то вроде:
access_control: - { path: ^/club, role: [ROLE_CLIENT] }
Потому, что разделение прав доступа должно происходить в интерфейсе пользователя и контроллере. «Эта кнопка у него доступна, эта недоступна» и тд.
В итоге:

// /src/Backend/CoreBundle/Security/Authorization/Voter/PrivilegeVoter.php
// ..
    public function supportsClass($class)
    {
        return in_array($class, array(
          'Backend\CoreBundle\Entity\SecurityContextResource'
       ));
    }
// ..


// /src/Backend/CoreBundle/Entity/SecurityContextResource.php

namespace Backend\CoreBundle\Entity;

class SecurityContextResource
{
    private $resourceId;
    
    public function __construct($resourceId)
    {
        $this->resourceId = $resourceId;
    }
    
    public function getResourceId()
    {
        return $this->resourceId;
    }
}


// /src/Backend/WorkorderBundle/Controller/DefaultController.php

//..
    private $resource;
    
    private function getResource()
    {
        if (!is_object($this->resource))
            $this->resource = new SecurityContextResource('/backend/workorder');

        return $this->resource;
    }
//..


Вызывать так $this->get('security.context')->isGranted('view', $this->getResource())
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации