пользователей: 30398
предметов: 12406
вопросов: 234839
Конспект-online
РЕГИСТРАЦИЯ ЭКСКУРСИЯ

Роли авторизованного пользователя

Как вам очевидно известно, RBAC — это управление доступом на основе ролей. Все, кто создавали системы чуть большие чем домашняя страничка и чуть меньшие чем Госуслуги, задумывались о том, как разграничить права пользователей.

 

В этой статье я не буду рассказывать о том, что такое RBAC и почему это хорошо (хотя немного, конечно, расскажу), а познакомлю вас со своей скромной разработкой (h-rbac) и попытаюсь объяснить, почему она по некоторым аспектам лучше, чем известные "монстры".

 

Вступление

 

Два столпа на которых держится RBAC — это роли и операции (role and permission). Слово "операция" мне нравится больше чем "разрешение", да и смысл отражает более верно. Выглядит это так:

 

Код → Операция → Роль → Пользователь

Таким образом в коде мы проверяем возможность выполнения операции:

 

if ($request->user()->can('add-to-favorites')) {

   // делаем что-то связанное с добавлением статьи в избранное

}

 

В свою очередь модуль RBAC выясняет, разрешена ли для данного пользователя указанная операция (т.е. содержится ли она в его роли) и, если да, то содержимое блока выполняется.

 

Ни в коем случае не стоит проверять в коде наличие у пользователя роли, т.к. сегодня "менеджер" может добавлять в избранное, а завтра уже не может. Искать по тексту и исключать из проверок отдельные роли — дело не благодарное, собственно для этого и существуют "операции".

 

Операция всегда непосредственно связана с блоком программы (см. пример выше) и при изменении ролей здесь точно ничего менять не придется. Зато в состав роли операции включаются и исключаются легко, просто и централизованно.

 

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

 

Мастодонты

 

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

 

 

Это действительно серьезные продукты имеющие много разных "плюшек" вроде переопределения операций для конкретного пользователя и т.п., но у меня к ним один простой вопрос (прощай карма, нам было хорошо вместе):

 

Как разрешить пользователю редактировать только свои статьи?

 

Хм… а никаких встроенных механизмов для этого нет! И это очень странно, т.к. в большинстве систем пользователи генерируют какой-то контент (например статьи) и должны иметь возможность его редактировать, при этом не имея доступа к редактированию чужого.

 

И еще: что если мой проект не такой уж большой? Что если весь этот геморрой с хранением ролей и операций в БД, связующими таблицами для "многие ко многим" и созданием целого UI для управления всем этим хозяйством излишен?
— Та-дам!

 

Большой секрет для маленькой...

 

Итак, пришло время поговорить о собственных проделках. Название статьи не случайно звучит "...RBAC для самых маленьких". Речь, конечно, не идет о ТТХ программистов, а скорее о размере проектов (и, конечно же, эта оценка весьма условная).

 

Если в вашем проекте не очень много операций и не очень много ролей и вы не против мечтали хранить их в виде массива, то рад представить вам модуль h-rbac (hierarchical RBAC with callbacks).

 

Нет никаких противоречий с тем, чтобы добавить к модулю разные провайдеры и хранить роли и операции хоть в БД, хоть в жп... любом другом интересующем вас месте.

Сразу хочу признаться, что принцип, использованный в данном модуле, был подсмотрен мной в Yii. Он был избавлен от ненужной (на мой взгляд) сущности Task и реального извращения в виде bizRule, вычисляющейся с помощью eval() (естественно, я говорю о Yii 1.1, в 2.0 стало по-другому, но, на мой взгляд, тоже довольно запутанно).

 

Требования

 

Для работы с модулем требуется Laravel 5.1 или выше.

 

Начать хочется с того, что стандартная система разрешений (операций) появилась в Laravel начиная с версии 5.1. Она имеет хорошую инфраструктуру, много полезных методов, поддерживается в blade и даже позволяет разрешить пользователю редактировать свои статьи (т.е. передавать в проверку аргументы), но(!) там напрочь отсутствуют роли...

 

… все пользователи равны — прямо демократия какая-то. Но мы-то с вами знаем, что обязательно должен быть кто-то "равнее" остальных. Иначе никак!

 

Так вот, модуль h-rbac, по сути, является надстройкой над стандартным механизмом авторизации (не путать с аутентификацией!), добавляющий туда роли, а также иерархию операций. Поэтому вы продолжите пользоваться абсолютно всеми стандартными "плюшками", но в контексте наличия ролей.

 

Установка

 

С помощью Composer

 

$ composer require dlnsk/h-rbac

 

Зарегистрируем провайдер в config/app.php

 

Dlnsk\HierarchicalRBAC\HRBACServiceProvider::class,

 

Опубликуем нужные нам элементы

 

$ php artisan vendor:publish --provider="Dlnsk\HierarchicalRBAC\HRBACServiceProvider"

 

а именно:

 

  • конфигурационный файл (config/h-rbac.php)
  • миграцию (добавляет текстовое поле role к таблице users)
  • класс конфигурации ролей, операций и колбэков (app/Classes/Authorization/AuthorizationClass.php)

 

Да, да, дорогие друзья, как сказано в заголовке, это модуль "для самых маленьких", поэтому у пользователя может быть только одна роль! Но с другой стороны, ведь роли — это всего лишь массив, добавить еще одну проще простого.

 

Блэкджек и девочки

 

Возьмем следующие роли:

 

  • admin
  • manager
  • user

 

и набор операций:

 

  • update-post
  • add-to-favorites

 

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

 

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

 

update-post → update-post-in-category → update-own-post

 

class AuthorizationClass extends Authorization

{

    public function getPermissions() {

        return [

            'update-post' => [

                    // Необязательное свойство "описание"

                    'description' => 'Редактирование любых статей',

                    // Используется для создания цепи (иерархии) операций

                    'next' => 'update-post-in-category',

                ],

            'update-post-in-category' => [

                    'description' => 'Редактирование статей в определенной категории',

                    'next' => 'update-own-post',

                ],

            'update-own-post' => [

                    'description' => 'Редактирование собственных статей',

                    // Здесь цепь заканчивается

                ],

            // Избранное

            'add-to-favorites' => [

                    'description' => 'Добавление статьи в список избранных',

                ],

        ];

    }

}

 

Назначение операций ролям очень простое. Кто что может делать очевидно:

 

class AuthorizationClass extends Authorization

{

    public function getRoles() {

        return [

            'admin' => [

                    'update-post',

                ],

            'manager' => [

                    'update-post-in-category',

                ],

            'user' => [

                    'update-own-post',

                    'add-to-favorites',

                ],

        ];

    }

}

 

Обратите внимание, что по существу у нас только две операции update-post и add-to-favorites именно возможность их выполнения мы и должны проверять. Вспомогательные операции update-post-in-category и update-own-post будут проверены автоматически, т.к. они являются содержимым одной с update-post цепи.

 

// PostController.php

 

class PostController extends Controller

{

    public function update(Post $post)

    {

        $this->authorize('update-post', $post);

        // продолжаем, если операция разрешена

    }

}

 

<!-- post-view.php -->

 

<h1>{{ $post->title }}</h1>

<ul class="post-tools">

    <li class="post author">{{ $post->author->username }}</li>

    @can('update-post', $post)

         <li class="post update"><button></button></li>

    @endcan

    @can('add-to-favorites')

         <li class="post add-favorite"><button></button></li>

    @endcan

</ul>

 

Операция add-to-favorites подразумевает возможность добавить в избранное любую статью, т.е. никаких дополнительных проверок делать не нужно, достаточно, чтобы эта операция содержалась в роли пользователя. По этой причине можно не передавать в проверку объект $post (но я бы передавал его в любом случае, т.к. сегодня дополнительных условий нет, а завтра могут появиться).

 

Осталось выяснить, как делать проверки тех самых дополнительных условий для update-post-in-category и update-own-post. Для этого достаточно добавить два метода (названия методов получаются путем камелкейсизации(о-па!) названия операции):

 

class AuthorizationClass extends Authorization

{

    public function updatePostInCategory($user, $post, $permission) {

        // Данный метод возвращает модель в случае, если $post содержит id модели

        $post = $this->getModel(\App\Post::class, $post);

 

        return $user->category_id === $post->category_id;

    }

 

    public function updateOwnPost($user, $post, $permission) {

        $post = $this->getModel(\App\Post::class, $post);

 

        return $user->id === $post->user_id;

    }

}

 

Параметр $permission в обоих этих методах будет содержать название изначально запрошенной операции update-post.

 

Логика проверки

 

Все операции находящиеся в цепи проверяются одна за другой в выбранном пользователем порядке и операция:

 

  • разрешается, если она содержится в роли и функция дополнительной проверки отсутствует;
  • разрешается, если она содержится в роли и функция дополнительной проверки возвращает true (проверка остальных операций цепи прекращается);
  • запрещается, если ни одна из операций цепи не содержится в роли;
  • запрещается, если для всех операций цепи, содержащихся в роли, функции дополнительной проверки вернули false.

 

Фишка для ленивых

 

Давайте добавим операцию delete-post. Логика подсказывает, что удалять пользователь может те статьи, которые он может редактировать. Подобная ситуация встречается достаточно часто и чтобы не создавать дополнительную цепь операций и абсолютно идентичные функции проверки аргумента воспользуемся одной хитростью — параметром equal:

 

class AuthorizationClass extends Authorization

{

    public function getPermissions() {

        return [

            'update-post' => [

                    // Необязательное свойство "описание"

                    'description' => 'Редактирование любых статей',

                    // Используется для создания цепи (иерархии) операций

                    'next' => 'update-post-in-category',

                ],

            'update-post-in-category' => [

                    'description' => 'Редактирование статей в определенной категории',

                    'next' => 'update-own-post',

                ],

            'update-own-post' => [

                    'description' => 'Редактирование собственных статей',

                    // Здесь цепь заканчивается

                ],

            // Избранное

            'add-to-favorites' => [

                    'description' => 'Добавление статьи в список избранных',

                ],

            // Удаление

            'delete-post' => [

                    'description' => 'Удаление статей',

                    'equal' => 'update-post',  // Применяем правила аналогичные редактированию

                ],

        ];

    }

 

    public function getRoles() {

        return [

            'admin' => [

                    'update-post',

                    'delete-post',

                ],

            'manager' => [

                    'update-post-in-category',

                ],

            'user' => [

                    'update-own-post',

                    'add-to-favorites',

                    'delete-post',

                ],

        ];

    }

}

 

Исходя из этого примера admin сможет удалять любые посты, user — только свои собственные, а вот manager не сможет удалить вообще ничего, т.к. в его роли нет операции delete-post, а значит и проверять ничего не требуется.

 

Пишите, Шура, пишите...

 

Как уже было сказано раньше, модуль является надстройкой над стандартной для Laravel 5.1 и выше системой авторизации, поэтому использовать его нужно так, как написано в документации, а если коротенько, то вот вам примерчики:

 

if (\Gate::allows('update-post', $post)) {

    // делаем что-нибудь, если это разрешено текущему пользователю

}

...

if (\Gate::denies('update-post', $post)) {

    abort(403);

}

...

if (\Gate::forUser($user)->allows('update-post', $post)) {

    // делаем что-нибудь, если это разрешено другому пользователю

}

 

Из модели User:

 

if ($request->user()->can('update-post', $post)) {

    // делаем что-нибудь

}

...

if ($request->user()->cannot('update-post', $post)) {

    abort(403);

}

 

В контроллере:

 

$this->authorize('update-post', $post);

 

С помощью Blade

 

@can('update-post', $post)

    <!-- Текущий пользователь может обновить статью -->

@else

    <!-- Текущий пользователь не может обновить статью -->

@endcan

 

@cannot('update-post', $post)

    <!-- Текущий пользователь не может обновить статью -->

@endcannot

 

Кроме того, специально для плохих мальчиков и девочек, добавлена дополнительная директива @role которую можно использовать вместе с @else

 

@role('user|manager')

    <!-- Текущий пользователь имеет любую из ролей -->

@endrole

 

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

 

Вся конфигурация операций и ролей в одном месте

 

P.S.:

 

Чуть не забыл… Названия ролей "admin", "manager" и "user" взяты в этой статье просто в качестве примера. На самом деле роль "admin" встроена в модуль и не требует определения. Она делает пользователя СУПЕРПОЛЬЗОВАТЕЛЕМ. К нему не применяются никакие проверки, ему разрешено абсолютно все. Ура, товарищи!

 


22.06.2017; 17:06
хиты: 164
рейтинг:0
для добавления комментариев необходимо авторизироваться.
  Copyright © 2013-2024. All Rights Reserved. помощь