Гибкая настройка разрешений для ролей RBAC

Робот и ключи

Практически в каждом проекте возникает необходимость реализовать регистрацию и авторизацию пользователей. В рецепте RBAC и описание ролей в файле подробно описана реализация распределения доступа пользователям по ролям. Этот рецепт является уточнением рецепта Аутентификация и авторизация, в котором и описано использование доступа по ролям. Попробуем сделать настройку ролей пользователей более гибкой.

В примере из рецепта обозначены четыре роли:

return array(
    'guest' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Гость',
        'bizRule' => null,
        'data' => null
    ),
 
    'user' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Пользователь',
        'children' => array(
            'guest',
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'moderator' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Модератор',
        'children' => array(
            'user',
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'administrator' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Администратор',
        'children' => array(
            'moderator', // позволим админу всё, что позволено модератору
        ),
        'bizRule' => null,
        'data' => null
    ),
);

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

class PostAdminController extends СController
{
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
 
    public function accessRules()
    {
        return array(
            array('allow',
                'roles'=>array('administrator'),
            ),
            array('deny',
                'users'=>array('*'),
            ),
        );
    }
}

и доступ к удалению комментариев только модераторам:

class CommentAdminController extends СController
{
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
 
    public function accessRules()
    {
        return array(
            array('allow',
                'roles'=>array('moderator'),
            ),
            array('deny',
                'users'=>array('*'),
            ),
        );
    }
}

Заметим, что администратор наследует права модератора, поэтому администратор тоже получит доступ к комментариям.

Здесь мы использовали простую иерархию и указывали разрешения на основе ролей:

Простая иерархия ролей

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

Для реализации такого разделения вместо разрешения по ролям удобно воспользоваться системой разрешений на основе операций.

Предположим, что у нас должны быть пять ролей:

  • Гость
  • Пользователь
  • Принятый пользователь
  • Модератор
  • Контент-менеджер
  • Администратор

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

Для реализации такого распределения стоит отказаться от разделения по именам ролей в пользу разделения по именам конкретных операций.

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

  • profile (доступ к личному кабинету)
  • forum (доступ к корпоративному форуму)
  • control_panel (доступ к панели управления)
  • m_pages (доступ к администрированию страниц)
  • m_blogs (доступ к администрированию блога)
  • m_comments (доступ к администрированию комментариев)
  • m_users (доступ к администрированию пользователей)

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

Сложная иерархия разрешений

Теперь в фильтрах доступа наших контроллеров помимо имён ролей можно использовать имена операций.

class PostAdminController extends СController
{
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
 
    public function accessRules()
    {
        return array(
            array('allow',
                'roles'=>array('m_blogs'),
            ),
            array('deny',
                'users'=>array('*'),
            ),
        );
    }
}
class CommentAdminController extends СController
{
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
 
    public function accessRules()
    {
        return array(
            array('allow',
                'roles'=>array('m_comments'),
            ),
            array('deny',
                'users'=>array('*'),
            ),
        );
    }
}

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

В файле auth.php для ролей и разрешений нужно использовать соответственно записи типов CAuthItem::TYPE_ROLE и CAuthItem::TYPE_OPERATION и прописать все наследования:

return array(
 
    /* Разрешения */
 
    'profile' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Доступ в личный кабинет',
        'bizRule' => null,
        'data' => null
    ),
 
    'forum' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Доступ в форум',
        'bizRule' => null,
        'data' => null
    ),
 
    'control_panel' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Доступ в панель управления',
        'bizRule' => null,
        'data' => null
    ),
 
    'm_comments' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Управление комментариями',
        'bizRule' => null,
        'data' => null
    ),
 
    'm_pages' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Управление страницами',
        'bizRule' => null,
        'data' => null
    ),
 
    'm_users' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Управление пользователями',
        'bizRule' => null,
        'data' => null
    ),
 
    'm_blogs' => array(
        'type' => CAuthItem::TYPE_OPERATION,
        'description' => 'Управление блогом',
        'bizRule' => null,
        'data' => null
    ),
 
    /* Роли */    
 
    'role_guest' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Гость',
        'bizRule' => null,
        'data' => null
    ),
 
    'role_user' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Пользователь',
        'children' => array(
            'role_guest',
            'profile'
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'role_active_user' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Принятый пользователь',
        'children' => array(
            'role_user',
            'forum'
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'role_moderator' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Модератор',
        'children' => array(
            'role_active_user',
            'control_panel',
            'm_comments',
            'm_users',
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'role_manager' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Контент-менеджер',
        'children' => array(
            'role_active_user',
            'control_panel',
            'm_blogs',
            'm_pages',
        ),
        'bizRule' => null,
        'data' => null
    ),
 
    'role_admin' => array(
        'type' => CAuthItem::TYPE_ROLE,
        'description' => 'Администратор',
        'children' => array(
            'role_active_user',
            'control_panel',
            'm_pages',
            'm_blogs',
            'm_comments',
            'm_users',
        ),
        'bizRule' => null,
        'data' => null
    ),
);

Если же необходимо раздавать правила для каждой операции (например, разрешить пользователю добавлять статьи, но не удалять), то можно пойти дальше к низкоуровневым задачам, например:

blog_read_post
blog_add_post
blog_edit_post
blog_edit_own_post
blog_delete_post

Потом ввести роли c наследованием данных задач:

Читатель
    blog_read_post
Редактор
    blog_add_post
    blog_edit_own_post
Модератор
    blog_add_post
    blog_edit_post
    blog_delete_post

Этот способ рассмотрен в другой подробной статье.

Теперь не нужно будет думать о переписывании кода контроллеров при каждом изменении прав. Все изменения можно будет легко делать в файле или таблице ролей.

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

Другие статьи

Любой программист с опытом осознаёт, что в неудачно спроектированном тяжёлом проекте изначально или со временем накапливается много неуправляемого и ненужного мусора. Это, например, повторяющийся код. В описании контроллера официального руководства Yii Framework указано, что Yii поддерживает вынос действий в отдельные классы и описывается процедура подключения этих действий к контроллерам. Но мало кто пользуется этим способом, так как не находит действительно тяжёлых повторяющихся экшенов.

При разработке интернет-магазинов или различных информеров для сайтов часто приходится реализовывать получение актуальных курсов валют. Не все разработчики знают, что достаточно удобно получать курсы на любую дату используя API сайта Центрального банка РФ.

Как многим известно, для хранения настроек приложения в Yii выделен специальный раздел `params` в конфигурационном файле. Это решение достаточно простое, но оно не позволяет легко менять настройки самому пользователю в панели управления сайта. Очередной вопрос на русском форуме Yii натолкнул меня поделиться своим вариантом решения упомянутого там вопроса.

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

Комментарии

 

MrArthur

Если такую систему строить, то уж лучше удобное расширение использовать.

Ответить

 

vovik

А вы не могли бы написать статью о реализации ulogin как в вашем блоге?

Ответить

 

Akulenok

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

Может статью напишите, тема очень актуальная.

Еще нравятся комментарии как у вас. Это расширение какое-то или вы сами написали?

Ответить

 

Akulenok

разобрался, проблема была в save() надо было false дорисовать

Ответить

 

Дмитрий Елисеев

Комментарии самодельные.

Ответить

 

Roman

Интересная статья. Спасибо.

А вот интересно, как в такой модели применять бизнес-правила?

Например, если нужно, чтобы к одной таблице имели доступ две роли: Admin и Moderator. Первый может все, Moderator - может только RU и то, только над своими. необходимо ввести правило по id, но теряется гибкость, и прийдется лезть в код действий контроллеров.

Ответить

 

Дмитрий Елисеев

Как раз для этого сделаны правила в виде классов в Yii2 и атрибут bizRule у правил в Yii1. Но да, придётся вставлять проверки в сами действия контроллеров.

Ответить

 

Антон

Большое спасибо, НО... А как эти роли передать пользователю из БД, чтобы механизм заработал на практике?

Это странно, но нигде не нашел этого или нашел, но какую-то странную реализацию. Как Вы это делаете?

Заранее спсибо за ответ.

Ответить

 

Дмитрий Елисеев

Кое-что описано здесь.

Ответить

 

Виктор

добрый день. скажите пожалуйста можно ли переделать фильтр accessControl или создать свой. который делал проверки и отправлял пользователя без прав доступа на заданную страницу. если можно скажите в какую сторону смотреть, чтобы такое сделать. потому что у меня пользователя редиретит из админки на site/login

Ответить

 

Дмитрий Елисеев

Можно определить deniedCallback для контроллера:

array('allow',
    'deniedCallback' => function() { Yii::app()->controller->redirect(array('/admin/mypage')); },
    'users' => array('@'),
),

Или в init() модуля админки заменить значение Yii::app()->user->loginUrl.

Ответить

 

Виктор

Еще такой вопрос. Проверка:

Yii::app()->admin->checkAccess('admin')

возвращает true. а если в accessRules добавить на экшн:

'roles' => array('admin')

то редирект на site/login. Почему эта проверка в данной функции не проходит?

Ответить

 

Дмитрий Елисеев

Yii::app()->admin или Yii::app()->user?

Ответить

 

Виктор

у меня объект admin для админки и user для сайта

Ответить

 

Дмитрий Елисеев

Вероятно, что вторая проверка в accessRules работает от user.

Ответить

 

Виктор

я тоже так думаю. хотел бы знать есть ли возможность изменить эту проверку?

Ответить

 

Виктор

помогло Yii::app()->setComponent('user', Yii::app()->admin); спасибо

Ответить

Оставить комментарий

Войти | Завести аккаунт


(никто не увидит)



Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>