Контент


A1/A2/Acl — больше, чем Auth

На электронную почту пришло письмо с просьбой более подробно (и менее сумбурно) описать набор модулей 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()).

Google Bookmarks Digg Reddit del.icio.us Ma.gnolia Technorati Slashdot Yahoo My Web News2.ru БобрДобр.ru RUmarkz Ваау! Memori.ru rucity.com МоёМесто.ru Mister Wong

Опубликовано в cправочник, Kohana3.

Теги: , , .


Комментарии (11)

Будьте в курсе обсуждения, подпишитесь на RSS ленту комментариев к этой записи.

  1. Андрей пишет:

    Спасибо за ответ на мое письмо, очень помог.

  2. Xobb пишет:

    Надо бы приглядется, сегодняшняя реализация Auth не сильная, вы правы.

  3. BIakaVeron пишет:

    Скажем так, она совсем простенькая — только фундамент. Этого мало для нормального проекта.

  4. taggi пишет:

    У меня при нажатии на кнопку Create account
    вываливается ошибка
    The email property does not exist in the User_Model class
    как я понял нужно email добавить в $ignored_columns ORM класса, но результата 0. Добавил указанное поле в БД ошибка пропала, после заполнения формы добавления пользователя ничего не произошло, запись в БД не добавилась… Что я не так делаю?

  5. BIakaVeron пишет:

    1. Проверьте, возможно библиотека Auth тоже подключена. Так как в ней тоже есть модель auth, могут быть проблемы.
    2. В библиотеке A1 по умолчанию нет поля email. Если оно есть в таблице БД, просто добавьте его в конфиг (параметр $config['columns']).

  6. Мáкса пишет:

    Иван, спасибо за статью. Авторизация — вещь полезная, но расписать вот так все подробно и доходчиво, это нужно иметь неплохую закалку :)
    Как всегда, на высоте!

  7. Alexis2004 пишет:

    Спасибо большое за обстоятельную статью по A1/A2/ACL!
    Стандартный Auth как уже упоминали выше ну никуда не годится.
    Очень понравился механизм разделения прав доступа и триггеров, без них невозможно организовать более-менее сложную систему.

    В связи с этим возник вопрос: Иван, вы не знаете случаем способа/библиотеки, чтобы подружить A1/A2/ACL от Wouterrr’а с OpenID? Вроде бы есть какие-то библиотеки, но они работают только с Auth и большинство из них для Kohana 2.xx, а я пишу на 3й версии.

  8. biakaveron пишет:

    Не интересовался, но ведь по идее интерфейсы A1 и Auth очень схожи (и вместо A1 можно Auth использовать с A2/Acl), так что портировать (в теории) должно быть нетрудно.

  9. HunterNomad пишет:

    Под 3.2 есть модуль ACL — kohana-deputy
    Интересно Ваше мнение про него.

  10. biakaveron пишет:

    @HunterNomad
    Не пользовался этим модулем, поэтому мнение еще не сформировано )) Мне всегда казалось, что интегрировать работу ACL и роутинга стоит все же через классы Route/Request. Не уверен, что перечислять вручную маршруты это хорошо (да, я видел, что там еще есть «звездочки»).

  11. HunterNomad пишет:

    Спасибо. Возможно Route/Request действительно правильное решение.



Можно включить подсветку кода: <code><pre lang="">...</pre></code>
Разрешены некоторые HTML теги

или используйте trackback.