Briefly in English
In this part of Jelly-Auth Kohana 3 tutorial I try to implement ACL. To be translated.
Итак, продолжаем допиливание макета админки, сделанного на Kohana 3 с помощью расширений Jelly и Jelly-Auth. В прошлый раз мы остановились на том, что мне нужна система управлением правами доступа. Вкратце система ролей может выглядеть примерно так:
- роль 'login' позволяет пользователю логиниться в админку. Фактически, она является признаком того, что пользователь активен, а для (временной) деактивации ее можно удалять;
- роль 'user' соответствует пользователю - не администратору. Позволяет редактировать свой профиль кроме назначения ролей;
- роль 'admin' позволяет осуществлять все возможные действия, за исключением редактирования паролей пользователей (ограничение весьма условное, однако для примера сгодится);
Из уже готовых расширений для Kohana 3, позволяющих реализовать достаточно сложную систему управления правами доступа я отметил для себя A2/Acl и Aacl. Первое внедряет в Кохану Зендовскую систему управления доступом, второе же придерживается другой идеологии, позволяя автоматически задавать доступ к целым классам. И хотя последнее нравится мне гораздо больше, но я не нашел как без своих патчей реализовать вывод элементов страниц в зависимости от разрешений на доступ. Поэтому пока что буду внедрять A2/ACL.
Предварительно рекомендую ознакомиться с руководством Ивана Броткина "A1/A2/Acl - больше, чем Auth"
Сначала я хотел использовать только ACL и , но потом решил попытаться пойти более простым путем, задействовав связку ACL-A2. Прежде всего, скачиваем модули ACL и A2 для Kohana 3. Затем в application/bootstrap.php добавляем
'a2' => MODPATH.'a2', 'acl' => MODPATH.'acl',
Копируем и заполняем конфигурационный файл application/config/a2.php. Нужно отметить, что применяемая для авторизации библиотека должна поддерживать метод get_user(), возвращающий FALSE если пользователь не определен и объект, реализующий интерфейс Acl_Role_Interface в противном случае, а также статический метод instance() для получения экземпляра объекта. Все это в Jelly-Auth есть.
'lib' => array( 'class' => 'Auth_Jelly', //'params' => array('auth') ),
'params' здесь не играет роли, поскольку Jelly-Auth, как и Auth, не принимает параметров в instance().
выбрасывать исключение при непрохождениии аутентификации мне не нужно
/** * Throws an a2_exception when authentication fails */ 'exception' => FALSE,
записываем систему ролей
'roles' => array ( 'user' => 'login', 'admin' => 'user', ), /* * The name of the guest role * Used when no user is logged in. */ 'guest_role' => 'guest',
Поскольку функциональность моей "админки" ограничивается сугубо работой с пользователями, то в качестве ресурса задаю только модель User
'resources' => array ( 'user' => NULL, ),
Наконец, правила (обратите внимание на пояснения в коде)
'rules' => array ( 'allow' => array ( array( 'role' => 'user', 'resource' => 'user', 'privilege' => 'edit', // 'user' может редактировать только свой профиль 'assertion' => array('Acl_Assert_Argument',array('id'=>'id')) ), array( // 'admin' может все кроме того, что запретим ниже в deny 'resource' => NULL, 'privilege' => NULL ), array( 'role' => 'user', 'resource' => 'user', // 'user' может редактировать только свой пароль 'privilege' => 'edit_pass', 'assertion' => array('Acl_Assert_Argument', array('id'=>'id')) ), ), 'deny' => array( array( 'role' => 'guest', // 'guest' не может ничего 'resource' => NULL, 'privilege' => NULL ), // админ не должен редактировать чужие пароли, // поэтому для редактирования паролей и создано отдельное правило array( 'role' => 'admin', 'resource' => 'user', 'privilege' => 'edit_pass', 'assertion' => array('Acl_Assert_NotArgument', array('id'=>'id')) ) ) )
Последнее правило в секции запрета (deny) означает запрет редактирования пароля если ID пользователя не равен ID админа. Можно было бы реализовать это ограничение и по-другому (раздельно давая разрешения админу на редактирование профиля и редактирование пароля), я так сделал, чтобы проиллюстрировать работу секции deny + создание собственных триггеров. Как видите, я использую триггер неравенства, который легко получается из триггера равенства:
class Acl_Assert_NotArgument implements Acl_Assert_Interface { protected $_arguments; public function __construct($arguments) { $this->_arguments = $arguments; } public function assert(Acl $acl, $role = null, $resource = null, $privilege = null) { foreach($this->_arguments as $role_key => $resource_key) { if($role->$role_key !== $resource->$resource_key) { return TRUE; } } return FALSE; } }
Далее нужно модифицировать модель пользователя, чтобы она смогла работать в связке с ACL. Для этого она должна реализовывать интерфейсы роли и ресурса (поскольку является как ролью, так и ресурсом). Соответственно, в классе нужно прописать обязательные методы интерфейсов get_role_id() и get_resource_id():
class Model_User extends Model_Auth_User implements Acl_Role_Interface, Acl_Resource_Interface { ... /** * because implements Acl_Role_Interface * */ public function get_role_id() { $roles = array(); $user_roles = $this->roles; foreach ($user_roles as $role) { $roles[] = $role->name; } return $roles; } /** * because implements Acl_Resource_Interface * */ public function get_resource_id() { return 'user'; } }
Наконец, добрались до модификации контроллера. Можно сделать так, чтобы необходимые изменения затронули лишь методы before() и action_users(). Правда, для этого сначала нужно пропатчить метод A2::logged_in(), который в оригинале не принимает роли в качестве параметра:
class A2 extends A2_Core { /** * Alias for logged_in in choosen auth library. * * @param string role name * @return boolean */ public function logged_in($role = NULL) { return $this->a1->logged_in($role = NULL); } }
Теперь мы сможем модифицировать метод before() нашего контроллера:
public function before() { parent::before(); $this->a2 = A2::instance('a2'); $this->auth = $this->a2->a1; $this->user = $this->auth->get_user(); $this->template->menu = $this->_auto_nav(); $this->template->footer = View::factory('admin/footer'); }
Как видите, вводится новое свойство класса $this->a2, которое отвечает за управление правами. Однако чтобы сделать апдэйт легче, отдельно присваиваю $this->auth = $this->a2->a1. Это дает возможность не переписывать код, работавший ранее с функционалом модуля Jelly-Auth.
В методе action_users() добавляются проверки разрешений:
public function action_users($action = NULL, $id = NULL) { ... switch ($action) { case 'add': $this->template->title = 'Add user'; $user = Jelly::factory('user'); // проверка разрешения на добавление юзера if ($this->a2->allowed($user, 'add')) { // добавляем пользователя } else { $this->template->content = 'You can not add users'; } break; case 'edit': $this->template->title = 'Edit user'; $user = Jelly::select('user') ->load($id); // проверка разрешения на редактирование юзера if ($this->a2->allowed($user, 'edit')) { ... if ($_POST) { ... // только администраторы могут менять роли if ($this->a2->allowed($user, 'edit_roles')) { // задание новых ролей } if ($this->a2->allowed($user, 'edit_pass')) { // задание нового пароля } // сохранение } ... } else { $this->template->content = 'You can not edit other users'; } break; case 'del': $this->template->title = 'Delete user'; $user = Jelly::factory('user'); // проверка разрешения на удаление юзера if ($this->a2->allowed($user, 'del')) { // удаление юзера } else { $this->template->content = 'You can not delete users'; } break; ... } } else { $this->template->title = 'User management'; // view user list $users = Jelly::select('user')->execute(); $this->template->content = View::factory('admin/user_list') ->set('user', $this->user) ->set('a2', $this->a2) ->set('users', $users); } }
Свойство $this->a2 понадобится нам в шаблоне, поскольку оно определяет видимость элементов управления. Например, видность элемента "Добавить пользователя" в шаблоне 'application/views/admin/user_list' определяется таким образом:
if ($a2->allowed($user, 'add')) { ?> <div id="add-user"> <a href="/admin/users/add">Add user</a> </div> <?php }
На удивление, все сразу заработало. Разумеется, назвать полнофункциональной данную ACL язык не поворачивается, однако уже виднеется некий прогресс.
Да, чуть не забыл, в таблицу ролей нужно вручную добавить роль, что-то типа:
INSERT INTO `roles` (`id`, `name`, `description`) VALUES (3, 'user', 'Simple user can only edit his own data');
Как обычно, исходники можно скачать / download 105 (zipped ~14 KiB)
Comments: 6 RSS
1 Zares 15-05-2010 15:12
Я недавно обнаружил неплохую заготовку для блога на KO3 с использованием Jelly и автозагрузкой представлений.
Довольно оригинально решена авторизация.
К сожалению никакого упоминания о нем в сети,
даже из поиска Github выпал :(
http://github.com/TdroL/schema-blog
Может обсудим данный вариант?...
2 Александр Купреев 17-05-2010 14:51
прошу простить, что долго не отвечал -- проблемы с провайдером :(
спасибо за ссылку, гляну
3 Александр Купреев 20-05-2010 20:08
Посмотрел. Действительно, вариант достаточно подробно проработанный, включая менеджмент страниц. Однако подход "роль=контроллер" и "роль=метод.контроллера" мне не очень нравится, поскольку не позволяет гибко модифицировать шаблоны в зависимости от прав пользователей. В aacl, как мне кажется, аналогичный подход.
4 Александр 02-03-2011 07:31
Здравствуйте, Александр. Отличная серия статей о kohana и jelly но вот остался у меня небольшой вопрос... Беру за каркас ваш исходник с этой статьи - вес замечательно, но кроме ресурса user у меня еще должен быть request и учетная запись (роль) - service - что я должен сделать?
1 - добавляем в конфиг a2 в resources "поле" request
2 - добавить туда же только в role service и унаследовать его от user?
3 - в контроллере есть функция action_request по аналогии с action_users
4 - создать модель для таблицы request
Все? или я что то пропустил - ведь не работает
5 Александр 02-03-2011 07:34
Забыл сказать что права назначены
но зайдя под этим юзером (с ролью service)
не срабатывает
6 Александр Купреев 03-03-2011 22:06
прошу простить за несвоевременный ответ.
В вашем случае
проверяет права текущего юзера (под которым работает контроллер) на добавление ресурса $user. Еслі нужно добавить request, то передайте в шаблон аналогичный объект $request и проверьте