На электронную почту пришло письмо с просьбой более подробно (и менее сумбурно) описать набор модулей A1/A2/Acl, которые я упоминал ранее. Поскольку Ko3 пока что не предоставляет достаточно поводов для «блогомарательства», предлагаю остановиться на данном замечательном инструменте.
О чем вообще речь?
Модули A1/A2/Acl предназначены для управления правами доступа пользователей. Дистрибутив Kohana v2.3 предоставляет нам только модуль Auth, который позволит контролировать минимальные доступные действия — регистрация, вход (login) и выход (logout). Но ведь помимо этого пользователь взаимодействует с различными объектами (ресурсами) сайта, и эти взаимодействия должны быть каким-то образом регламентированы. Самый простой пример — блог. Гости должны иметь возможность читать записи, авторизованные пользователи также могут комментировать, а администратор (владелец блога) — создавать записи и редактировать комментарии.
Подготовка к работе
Для ветки 2.3.x модули расположены здесь. Нас интересуют модули A1, A2 и Acl, а также демка с примером возможностей модулей A2 и Acl (она называется a2-acl-demo). Просто подкладываем их в папку MODPATH и добавляем в список модулей в config.php. Если запустить метод db контроллера a2demo (http://localhost/a2demo/db), то мы увидим минимально необходимую схему БД для дальнейшей работы с демо-модулем:
CREATE TABLE IF NOT EXISTS `users` ( `id` int(12) unsigned NOT NULL auto_increment, `username` varchar(32) NOT NULL default '', `password` char(50) NOT NULL, `token` varchar(32) default NULL, `logins` int(10) unsigned NOT NULL default '0', `last_login` int(10) unsigned default NULL, `role` enum('user','admin') NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `blogs` ( `id` int(12) unsigned NOT NULL auto_increment, `user_id` int(12) unsigned NOT NULL, `text` text NOT NULL, PRIMARY KEY (`id`), KEY `user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
Т.е. необходимо создать таблицу для пользователей (она несильно отличается от используемой в Auth) и таблицу для сохранения наших разделяемых ресурсов (т.е. записей блога). Естественно, для настоящего блога эта табличка уж очень неактуальна, но сейчас цель — показать принцип работы модулей. Сами таблицы можно не заполнять — демо-контроллер позволит нам зарегистрироваться и немного поработать над блогом.
Знакомство с демо-версией
Откроем http://localhost/a2demo/. Поскольку мы не представились системе, нам предлагают войти или зарегистрироваться.
Выберем второй вариант и получим следующую форму
Как мы видим, необходимо ввести логин, пароль (дважды) и указать роль. Доступна роль обычного пользователя (user) и администратора (admin). Ниже выведена информация о содержимом массива $_POST и переменной $user, в которой будет сохранен пользователь. Не обращайте на нее внимания
После успешной регистрации мы автоматически авторизовываемся и отправляемся на главную страницу демки. Теперь мы можем собственно управлять ресурсами. Вот так выглядит страница глазами обычного пользователя (я уже создал несколько записей в блоге):
Помимо собственно добавления записей предлагается изменить или удалить существующие. Конечно, только администратор имеет возможность управлять всеми записями — пользователь может редактировать или удалять только свои записи.
А теперь пришло время разобраться, как это все работает.
Второй демо-контроллер, описывающий работу модуля Acl, я тут показывать не буду, т.к. он гораздо менее информативен. После прочтения данной статьи Вы можете проанализировать его самостоятельно.
A1: аутентификация
Если Вы знакомы с модулем Auth, то все здесь будет понятно. Библиотека A1 предоставляет базовые методы login(), logout() и get_user(). Все знакомо. Настройка библиотеки хранится в конфигурационном файле (его имя передается в конструктор, по умолчанию ‘a1‘). Там тоже все более-менее понятно, новым для нас будет ключ ‘columns‘:
/** * Table column names */ $config['columns'] = array( 'username' => 'username', //username 'password' => 'password', //password 'token' => 'token', //token 'last_login'=> 'last_login', //last login (optional) 'logins' => 'logins' //login count (optional) ); |
Как понятно из названия, тут описываются имена столбцов в БД. Из обязательных только первые три (если не планируется использовать «запоминание» пользователей, то и ‘token‘ не нужен). Это нужно для совместимости со сторонними таблицами пользователей.
В модуле определена абстрактная модель A1_User, от которой необходимо будет наследовать модель для реальной таблицы пользователей (по аналогии с Auth, там была абстрактная модель Auth_User). Имя модели пользователя указывается в параметре ‘user_model‘ (по умолчанию конечно же ‘user‘).
Acl: списки управления доступом
Библиотека Acl пришла в Kohana из фреймворка Zend и предполагает работу с тремя базовыми терминами: роль, ресурс и действие. Например, в условии «редактирование администратором записи в блоге» ролью будет ‘admin‘, действием — ‘edit‘ (редактирование), а ресурсом — запись в блоге (‘blog‘). Термины абстрактны, т.е. под записью в блоге будет рассматриваться любая запись, независимо от условий ее создания (автор, дата и т.д.), а под ролью ‘admin‘ — любой пользователь с данной ролью. Данные три термина объединяются в правила, которые определяют порядок взаимодействия с ресурсами. При этом все, что не разрешено — запрещено.
Сперва объявляем известные нам роли и ресурсы:
// создаем объект Acl $acl = new Acl; // добавляем роли $acl->add_role('guest'); // гость $acl->add_role('user'); // пользователь $acl->add_role('admin'); // администратор // добавляем ресурсы $acl->add_resource('blog'); // блог $acl->add_resource('comment'); // комментарий |
Правила добавляются с помощью методов allow() и deny() объекта Acl:
// изначально все запрещено // писать необязательно, привел для примера $acl->deny(NULL, NULL, NULL); // все могут читать все $acl->allow(NULL, NULL, 'read'); // пользователь и админ могут писать комментарии $acl->allow(array('user', 'admin'), 'comment', 'write'); // админ может писать в блог $acl->allow('admin', 'blog', 'write'); |
В правилах сперва указывается роль (или массив ролей), затем ресурс (или ресурсы), потом — действие (действия). Вместо перечисления всех ролей/ресурсов/действий можно указать NULL (так мы дали права на чтение в примере выше). Есть еще и четвертый параметр, но о нем поговорим отдельно.
Поскольку по умолчанию все запрещено, мы идем по пути «добавляем все, что можно». Другой вариант — можно разрешить все (
$acl->allow(NULL, NULL, NULL)
), а затем добавить запрещающие правила. Главный минус — потом можно легко об этом забыть и оставить дыры в защите.
Проверяем права доступа:
var_dump($acl->is_allowed('guest', 'blog', 'write')); // false var_dump($acl->is_allowed('user', 'comment', 'write')); // true var_dump($acl->is_allowed('admin', 'blog', 'write')); // true |
Использование иерархии
Перечисление всех имеющихся ролей и ресурсов при добавлении правил может превратиться в весьма утомительное занятие. Поэтому библиотека предоставляет возможность построения иерархий с наследованием правил от предка к потомку. Давайте переделаем вышеописанный пример:
// создаем объект Acl $acl = new Acl; // добавляем роли $acl->add_role('guest'); // гость $acl->add_role('user', 'guest'); // пользователь $acl->add_role('admin', 'user'); // администратор // добавляем ресурсы $acl->add_resource('text'); // абстрактный ресурс $acl->add_resource('blog', 'text'); // блог $acl->add_resource('comment', 'text'); // комментарий |
Как видно, была построена иерархия ролей (гость -> пользователь -> админ) и ресурсов (текст -> блог и текст -> коммент). Теперь все правила, примененные для гостя, будут доступны и пользователю с админом, а правила для текста могут быть использованы блогом и комментарием. Соответственно так теперь будем добавлять правила:
// все могут читать все $acl->allow('guest', 'text', 'read'); // пользователь и админ могут писать комментарии $acl->allow('user', 'comment', 'write'); // админ может писать в блог $acl->allow('admin', 'blog', 'write'); |
Если в правиле указан предок, потомков можно убирать (наследование в чистом виде). В первом правиле я заменил NULL на ‘text‘ — результат тот же, но само правило более четкое. Например, если добавится новый ресурс ‘stats‘, который должен быть разрешен только администраторам, общее правило с NULL позволит любому пользователю получить доступ. В общем, не ленитесь уточнять правила, чтобы потом после разрастания проекта это где-нибудь не аукнулось.
Проверка объектов
До этого мы говорили про абстрактные ресурсы и роли (в виде строк). Однако библиотека поддерживает передачу объектов, например можно использовать что-то вроде $acl->is_allowed($user, 'comment', 'write');
В этом случае объект $user (в данном случае имелся в виду экземпляр ORM-модели User_Model) должен реализовывать интерфейс Acl_Role_Interface, что заключается в наличии метода get_role_id():
class User_Model extends A1_User_Model implements Acl_Role_Interface { public function get_role_id() {// мы должны вернуть значение роли return $this->role; } |
В данном случае возвращается значение поля role, т.к. оно присутствует в реализации библиотеки A1. Если говорить про Auth, там пришлось бы возвращать массив ролей из таблицы roles. Аналогично можно передавать ресурсы, только для них интерфейс называется Acl_Resource_Interface, а необходимый метод — get_resource_id().
class Blog_Model extends ORM implements Acl_Resource_Interface { public function get_resource_id() { return 'blog'; } } |
Assertions
Казалось бы, зачем нам все эти сложности с передачей объектов, если правила Acl поддерживают только абстрактные строки? И вот тут на сцену выходят т.н. assertions (что-то вроде триггеров, дальше я буду использовать именно такой термин). Данные триггеры позволяют совершать дополнительные проверки с полученными объектами. Вот как реализовывается разрешение редактирования собственных комментов пользователем:
$acl->allow('user','comment','edit',array('Acl_Assert_Argument',array('id'=>'user_id'))); |
В результате при проверке $acl->is_allowed($user, $comment, 'edit')
будет выполнена проверка на равенство $user->id
и $comment->user_id
(т.е. на принадлежность комментария текущему пользователю). Acl_Assert_Argument — это имеющийся в модуле Acl класс, позволяющий связывать между собой поля из моделей роли (в данном случае это модель User_Model, поле id) и ресурса (Comment_Model, поле user_id). Имена полей передаются в виде массива после имени класса. Можно использовать несколько связок, например так:
$acl->allow('user','comment','edit',array('Acl_Assert_Argument',array('id'=>'user_id', 'something_else' => 'other_field'))); |
Если все пары полей равны между собой, проверка вернет TRUE и доступ будет получен.
Конечно, это очень простой пример триггера, который не может решить всех проблем. Давайте попробуем реализовать простую проверку — создавать блог может только пользователь с количеством комментариев больше 100.
1. Каждый класс-триггер должен реализовывать интерфейс Acl_Assert_Interface. Это заключается в необходимости реализовать метод assert(), объявленный в интерфейсе следующим образом:
public function assert(Acl $acl, $role = null, $resource = null, $privilege = null); |
Собственно говоря, данный метод как раз и вызывается при проверке правила. Таким образом, получим первый шаг на пути к созданию триггера:
class Acl_Assert_Comments implements Acl_Assert_Interface { public function assert(Acl $acl, $role = null, $resource = null, $privilege = null) { } } |
2.Если необходимо предварительно настроить триггер (в примере выше сперва должны были быть созданы соответствия пар полей из объектов роли и ресурса), используем конструктор. При создании правила, как мы помним, передается дополнительный параметр, который мы и будем обрабатывать:
class Acl_Assert_Comments implements Acl_Assert_Interface { protected $comments_required; public function __construct($arguments) { is_array($arguments) AND $arguments = $arguments[0]; $this->comments_required = intval($arguments); } public function assert(Acl $acl, $role = null, $resource = null, $privilege = null) { } } |
Мы сохраняем переданное в конструктор значение (количество комментариев для ведения записей в блоге) в свойстве $comments_required. Далее в методе assert() оно будет использовано.
3. Осталось только реализовать сравнение количества комментов у пользователя с сохраненным значением:
class Acl_Assert_Comments implements Acl_Assert_Interface { protected $comments_required; public function __construct($arguments) { is_array($arguments) AND $arguments = $arguments[0]; $this->comments_required = intval($arguments); } public function assert(Acl $acl, $role = null, $resource = null, $privilege = null) { if ($role->comment_count < $this->comments_required) return FALSE; return TRUE; } } |
В данном примере поле comment_count присутствует в таблице users, так как я стараюсь избавиться от регулярного пересчета подобных статистических данных. Естественно, никто не мешает использовать что-то вроде
$role->comments->count()
.
4. Вот и все, триггер создан и работает — можете проверять. Добавление правила будет выглядеть так:
$acl->allow('user', 'blog', 'comment', new Acl_Assert_Comments(100)); |
A2
По сути рассмотренные выше классы A1 и Acl между собой никак не свзяаны — один отвечает за «узнавание» пользователя, а второй «ставит его на место». Для удобства работы с ними был создан класс-обертка A2. Он является потомком класса Acl и имеет все те же методы для работы с правилами. В качестве дополнения он агрегирует объект A1, что позволяет работать с текущим пользователем напрямую.
Какие методы нам предоставляет класс A2:
1. Разнообразные варианты создания объекта ( __construct(), factory() и instance() ). Все они принимают в качестве единственного параметра $config_name — имя файла конфигурации (об этом чуть ниже).
2. Методы для работы с объектом A1.
2.1. Метод logged_in() перенаправляет вызов к объекту A1, в результате получаем проверку залогиненности пользователя.
2.2. Метод get_user() таким же макаром возвращает сам объект модели пользователя (обычно это User_Model).
3. Метод allowed($resource = NULL, $privilige = NULL)
проверяет, может ли текущий пользователь иметь возможность применить к ресурсу $resource действие $privilige. Таким образом, можно не передавать вручную в объект Acl пользователя, а использовать вот такую сокращенную запись.
Конфигурация
Самое интересное, что наверняка бросилось в глаза — наличие некого параметра $config_name в конструкторе. Да-да, можно не прописывать правила, роли и ресурсы в контроллере, а использовать заранее сохраненные настройки. Вот так вот будут выглядеть настройки, описанные нами выше:
$config['roles'] = array ( 'user' => 'guest', 'admin' => 'user' ); $config['guest_role'] = 'guest'; $config['resources'] = array ( 'text' => NULL, 'blog' => 'text', 'comment' => 'text', ); $config['rules'] = array ( 'allow' => array ( array('guest','text','read'), array('user', 'comment', 'write'), array('admin', 'blog', 'write'), array('user', 'blog', 'write', array('Acl_Assert_Comments', array(100))), ), ); |
Все довольно просто и понятно. Параметр $config['guest_role']
определяет имя роли, которая будет использоваться, если никто не залогился. Есть еще один дополнительный параметр, определяющий библиотеку для работы с аутентификацией (я говорил про A1, но ведь и Auth никто не отменял):
$config['a1'] = array('A1'); |
Если планируется использование другой библиотеки, то необходимо убедиться, что она реализует метод get_user() для получения текущего пользователя, а также использует шаблон Singleton — т.е. имеет статический метод instance(). В качестве параметра после имени библиотеки можно указать аргументы для передачи в метод instance(), обычно это имя конфигурации библиотеки. Сам объект будет сохранен в свойстве $a1 (оно объявлено как public).
Постскриптум
Вот собственно и все. В использовании A2 похожа на Acl, дополнительные возможности я указал. Самая неочевидная ошибка, на которую сам неоднократно напарывался — частенько забывал в моделях ролей и ресурсов реализовывать интерфейсы Acl_Role_Interface и Acl_Resource_Interface — они должны возвращать имя соответственно текущей роли (метод get_role_id()) или ресурса (get_resource_id()).
Спасибо за ответ на мое письмо, очень помог.
Надо бы приглядется, сегодняшняя реализация Auth не сильная, вы правы.
Скажем так, она совсем простенькая — только фундамент. Этого мало для нормального проекта.
У меня при нажатии на кнопку Create account
вываливается ошибка
The email property does not exist in the User_Model class
как я понял нужно email добавить в $ignored_columns ORM класса, но результата 0. Добавил указанное поле в БД ошибка пропала, после заполнения формы добавления пользователя ничего не произошло, запись в БД не добавилась… Что я не так делаю?
1. Проверьте, возможно библиотека Auth тоже подключена. Так как в ней тоже есть модель auth, могут быть проблемы.
2. В библиотеке A1 по умолчанию нет поля email. Если оно есть в таблице БД, просто добавьте его в конфиг (параметр $config['columns']).
Иван, спасибо за статью. Авторизация — вещь полезная, но расписать вот так все подробно и доходчиво, это нужно иметь неплохую закалку
Как всегда, на высоте!
Спасибо большое за обстоятельную статью по A1/A2/ACL!
Стандартный Auth как уже упоминали выше ну никуда не годится.
Очень понравился механизм разделения прав доступа и триггеров, без них невозможно организовать более-менее сложную систему.
В связи с этим возник вопрос: Иван, вы не знаете случаем способа/библиотеки, чтобы подружить A1/A2/ACL от Wouterrr’а с OpenID? Вроде бы есть какие-то библиотеки, но они работают только с Auth и большинство из них для Kohana 2.xx, а я пишу на 3й версии.
Не интересовался, но ведь по идее интерфейсы A1 и Auth очень схожи (и вместо A1 можно Auth использовать с A2/Acl), так что портировать (в теории) должно быть нетрудно.
Под 3.2 есть модуль ACL — kohana-deputy
Интересно Ваше мнение про него.
@HunterNomad
Не пользовался этим модулем, поэтому мнение еще не сформировано )) Мне всегда казалось, что интегрировать работу ACL и роутинга стоит все же через классы Route/Request. Не уверен, что перечислять вручную маршруты это хорошо (да, я видел, что там еще есть «звездочки»).
Спасибо. Возможно Route/Request действительно правильное решение.