framework.zend.com
Stable релиз 2.0 / 1.12

Zend_Acl часть 3: создание и хранение динамических ACL

К комментариям

Введение.

Данный текст - перевод статьи Zend_Acl part 3: creating and storing dynamic ACLs автором которой является Jani Hartikainen.

В третьей части я расскажу об использовании динамических ACL. Мы познакомимся с тем как хранить ACL в базе данных, и как по необходимости извлекать их от туда. Эта статья базируется на способах работы с Zend_Acl про которые я рассказал в первой и второй части.

Сначала мы рассмотрим простой пример с "пользователями" и "страницами". Затем комплексный пример, включающий создание более сложных ACL c наследованием, различными типами ролей и тд.

Предпосылки к созданию динамических ACL.

Ранее мы рассматривали ACL которые жёстко закодированы ( hardcoded - http://en.wikipedia.org/wiki/Hard_coding ), теперь мы рассмотрим создание "динамических" ACL. Статические Acl хорошо подходят для быстрого создания простых сайтов. Но использование статических ACL начинает терять эффективность в тот момент, когда вам становится необходимым предоставить администраторам возможность управлять правами доступа с помощью некоторого интерфейса панели администрирования.

К тому же, при большом количестве пользователей или ролей, будет не лучшей идеей постоянно создавать "полный" ACL Одним из рассматриваемых в этой статье вопросов является: создание ACL содержащих только те элементы, которые необходимо для осуществления конкретной проверки доступа.

Давайте предположим: у нас есть пользователь, и нам необходимо проверить доступ к конкретной странице. Будет ли логично загружать весь ACL, со всеми пользователями, страницами, а затем устанавливать у них у всех права доступа? Нет, конечно нет. Лучше создать ACL только с одним пользователям, и одним ресурсом для которого необходимо произвести проверку.

Простой пример с пользователями и страницами.

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

zend_acl схема базы данных

На рисунке 1. представлена схема базы данных для данного примера. Как вы смогли заметить она достаточна проста, всего три таблицы.

  1. Таблица pages ( страницы ) содержит страницы нашего сайта
  2. Таблица users ( пользователи ) содержит данные наших пользователей
  3. Таблица Page_privileges ( привилегии к страницам ) содержит данные о том каким пользователям к каким страница разрешён доступ.

В данном случае, будет удобно представить роли и ресурсы в виде чисел: каждый пользователь – роль, и каждая страница – ресурс. Таким образом, мы легко можем представить их в ACL, используя их соответствующие идентификаторы ( id ) из базы данных.

ACL factory ( http://ru.wikipedia.org/wiki/Factory )

Перед тем как продолжить, я хочу представить вам концепцию, которую я буду использовать на протяжении всей статьи – это ACL factory класс. Этот класс позволит нам улучшить контроль кода который создаёт ACL, и позволит хранить всё что связанно с инициализацией ACL в одном месте. Мы так же в дальнейшем сможем указать имена таблиц и прочее.

class AclFactory {
/**
* Создаёт ACL для указанной страницы
* @param Page $page
* @return Zend_Acl
*/
public function createAcl(Page $page) {

// давайте предположим у нас есть модель для таблицы page_privileges c
// похожим методом, который вернёт объект PagePrivilege, в качестве
// параметра укажем идентификатор страницы
$privileges = PagePrivilege::findByPageId($page->getId());

$acl = new Zend_Acl();
$acl->add(new Zend_Acl_Resource($page->getId()));

foreach($privileges as $privilege) {
$acl->addRole(new Zend_Acl_Role($privilege->getUserId()));
$acl->allow($privilege->getUserId(), $page->getId());
}

return $acl;
}
}

Метод работает следующим образом: сначала выбираем все привилегии к странице. Затем создаём новый экземпляр Zend_Acl, и добавляем ресурс с идентификатором страницы. Далее в цикле добавляем роли и разрешаем им доступ к созданному ресурсу.

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

Но как это работает? Так как мы используем идентификатор записи соответствующей пользователю в качестве идентификатора роли, мы можем обойти в цикле все найденные привилегии и добавить идентификатор пользователя в ACL. Таким образом ACL будет содержать все возможные роли для ресурса. Далее, так как привилегия означает то, что пользователю разрешён доступ к странице, мы так же разрешаем его в ACL.

Использование ACL factory.

Теперь, после того как мы написали код который создаёт ACL, давайте посмотрим как его можно использовать.

// $page – страница к которой мы хотим получить доступ.
$factory = new AclFactory();
$acl = $factory->createAcl($page);

// $user – текущий аутентифицированный пользователь.
if($acl->hasRole($user->getId()) && $acl->isAllowed($user->getId(), $page->getId()) {
// Некоторый код который обрабатывает успешный доступ
}

Итак, мы с помощью фабрики создали ACL, который затем использовали уже привычным для нас образом. Так как АСL содержит только те роли которые имеют доступ к странице, мы использовали метод hasRole чтобы убедится что ACL содержит текущую роль. Дело в том что если роли не существует метод isAllowed() вызовет исключение (exeption).

Комплексный пример: Каталог файлов.

Итак, мы изучили основы, пора погрузится глубже: Как реализовать ACL для системы которая будет предоставлять пользователям файлы.

zend_acl схема базы данных

Требования к системе разграничения доступа к файловой системе:

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

Таким образом если говорить в терминологии Zend_Acl категории и файлы – это ресурсы. Файлы будут наследовать права доступа от категорий к которым они относятся, а категории будут наследовать их права от родителей.

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

Предустановки

Введём несколько соглашений. У нас будет одна предустановленная группа в базе данных – гости ( guests ).

Не аутентифицированные пользователем будет присваиваться идентификатор ( identity ), который относится к группе гость. Таким образом при желании мы сможем с помощью ACL ограничить доступ для гостей.

У нас так же есть классы – модели которые представляют каждую из таблиц базы данных. В этих классах есть геттеры и сеттеры. Они написаны в том же стиле что и класс NewsPost из статьи practical uses for reflection

Начнём

Основные действия будут проходить в классе ACL factory. Мы изменим представленный ранее класс таким образом, чтобы он смог удовлетворить требованиям нашего комплексного примера. Мы добавим два открытых ( public ) метода createCategoryAcl() и createFileAcl(). Рассмотрим первый из них.

public function createCategoryAcl(Category $category, User $user) {
$acl = new Zend_Acl();
$this->_createRoles($acl, $user);

$categories = $category->getParents() + array($category);

// самая верхняя категория не имеет родителя, поэтому в начале null
$parent = null;
foreach($categories as $c) {
$acl->add($c, $parent);
// следующий будет иметь родителя
$parent = $c;
}

$privileges = Privilege::findByCategories($categories);
$this->_setPrivileges($acl, $privileges);
return $acl;
}

Метод принимает в качестве параметров объект Category (категория) и объект User (пользователь). Мы рассмотрим методы _createRoles() и _setPrivileges() позже.

Класс Category реализует интерфейс Zend_Acl_Resource_Interface так как было показано во второй части. Таким образом фабричный класс при создании ACL может без дополнительных преобразований передавать объекты в метод Zend_Acl::add().

Здесь так же показано как можно реализовать наследование: мы выбираем всех родителей категории и наследуем одного от другого путём передачи их в метод add().

Метод getParents() возвращает всё древо родителей. Первым элементом массива будет самый старший(верхний) предок, вторым его потомок и так далее. Корневой элемент (категория) не имеет родителя, поэтому мы обозначали её как null. При обходе массива категорий мы изменяем родительскую категорию на текущую, таким образом мы передаём текущую категорию как родительскую для следующей.

Метод findByCategories() класса Privilege возвращает все объекты - привилегии для определённой категории. Просмотрите схему базы данных чтобы понять что это из себя представляет.

Скрытые методы (private методы) .

В методе createCategoryAcl() происходит вызов метода _createRoles()

Задача этого метода – назначить роли для объекта пользователь, который был передан в качестве параметра.

private function _createRoles(Zend_Acl $acl, User $user) {

// сначала добавляем группы, таким образом пользователь наследует их в
// ACL
foreach($user->getGroups() as $group) {
$acl->addRole($group);
}

$acl->addRole($user, $user->getGroups());
}

Итак вызывая этот метод мы инициализируем в ACL все роли пользователя. Подобно классу Category, классы User и Group реализуют Zend_Acl_Role_Interface, таким образом, мы без труда можем их передавать в метод addRole(). В конце статьи приведена ссылка на исходный код, вы можете ознакомиться с ним для лучшего понимания устройства этих классов.

Метод _setPrivileges() немного сложнее. Его задача заключается в просмотре объекта Privilege и при необходимости разрешить или запретить доступ к ресурсу.

Мы передаём в качестве входного параметра объект Privilege, который хранит в себе ресурс, роль, и тип привилегии (разрешено, запрещено). Основываясь на этих данных _setPrivileges() при наличии в ACL необходимой роли разрешает или запрещает ей доступ к ресурсу.

private function _setPrivileges($acl, $privileges) {
foreach($privileges as $privilege) {
$role = $privilege->getRole();
$resource = $privilege->getResource();
// роль должна быть уже созданна, таким образом мы можем
// перейти на следующий шаг, если роли нет.
if(!$acl->hasRole($role)) {
continue;
}

if($privilege->getMode() == 'allow')
$acl->allow($role, $resource);
else
$acl->deny($role, $resource);
}
}

У класса Privilege есть методы которые возвращают связанные с ним объекты типа роль и ресурс. В зависимости от типа роль может является как объектом класса Group, так и объектом класса User. Аналогично ресурс может быть как объектом класса Category так и объектом класса File. Так как они все реализуют соответствующие интерфейсы, они могут быть напрямую переданы в соответствующие методы Zend_Acl.

ACL для файлов.

Теперь рассмотрим другие, "открытые" (public) методы класса.

public function createFileAcl(File $file, User $user) {
$acl = null;
if($file->getCategoryId() != null) {
// если файл относится к какой то категории,
// нам необходимо создать всю категорию основанную на асl
$category = $file->getCategory();
$acl = $this->createCategoryAcl($category, $user);
$acl->add($file, $category);
}
else {
$acl = new Zend_Acl();
$this->_createRoles($acl, $user);
$acl->add($file);
}

$privileges = Privilege::findByFile($file);
$this->_setPrivileges($acl, $privileges);
return $acl;
}

В связи с тем что файл может принадлежать какой – либо категории нам необходимо в первую очередь создать в ACL категорию. В том случае если файл не относится к какой либо категории, мы добавляем его на верхний уровень, то есть он не будет наследовать какие либо привилегии от родительской категории. Файл сам по себе может иметь какие – либо привилегии доступа к нему, таким образом мы извлекаем эти привилегии и вызываем метод _setPrivileges() для их обработки.

Последние строки кода.

Итак, у нас есть один или несколько классов АСL Теперь для наших нужд создадим специальный плагин. Здесь всё очень просто, думаю Вам будет не сложно разобраться.

<?php
class App_Plugin_AccessCheck extends Zend_Controller_Plugin_Abstract {
public function preDispatch(Zend_Controller_Request_Abstract $request) {
if(!$this->_accessValid($request)) {
// Вызываем исключение,тк контроллер ошибок (error controller) сможет
// перехватить это исключение
throw new App_Exception_AccessDenied('Access denied');
}
}

private function _accessValid(Zend_Controller_Request_Abstract $request) {
$user = $request->getParam('user', null);
// плагин который загружает идентификаторы должен был добавить
// пользователя в объект запроса, если его там нет значит что то не так
if($user === null)
return false;

$params = $request->getParams();
$factory = new App_AclFactory();
$resource = null;

if(isset($params['file'])) {
$resource = File::findById($params['file']);
$acl = $factory->createFileAcl($resource, $user);
}
elseif(isset($params['category'])) {
$resource = Category::findById($params['category']);
$acl = $factory->createCategoryAcl($resource, $user);
}
else {
// так как в нашем примере мы работаем только с категориями и файлами
// мы возращаем true если доступ происходит к чему то другому
return true;
}

return $acl->hasRole($user) && $acl->has($resource)
&& $acl->isAllowed($user, $resource);
}
}

Это плагин который будет использовать ACL для проверки доступа. Как мы и договорились ранее пользователь который не вошёл в систему автоматически относится к группе "гость", таким образом у нас есть код который загружает объект – пользователь (User) в объект запроса. Мы используем этот параметр в методе _accessValid(), где проверяем его привилегии согласно ACL.

Здесь так же используются некоторые другие параметры запроса, которые позволяют определить к чему запрашивается доступ, к категории или файлу.

В заключение.

Итак, теперь вы увидели более комплексный пример использования Zend_Acl. я не затронул в статье использованные модели, конструкции SQL и тд. Тк для вас это вероятно не является сложным и не представляет интереса. И не пугайтесь, весь исходный код доступен для загрузки.

Загрузить исходный код

я сопроводил комментариями некоторую часть кода, но я должен Вас предупредить: он возможно далёк от оптимального. Он не всегда использует безопасные sql- запросы, в некоторых местах есть излишние запросы, и в нём нет должной защиты от ошибок. Но на мой взгляд он выполняет своё основное предназначение: показать вам как реализовать динамическую систему ACL. И ещё… я не реализовал панель администрирования, так что все необходимые тестовые данные вам придётся загрузить непосредственно через интерфейс базы данных.

Продолжение серий данных статей не планируется, все ваши вопросы вы можете задать автору или на форуме

P.S. Вы могли заметить что в статьях не расказано о Zend_Acl_Assertions, тк я не представляю как их использовать.

Тема на форуме.

метки: Zend_Acl, ACL
Лучший способ следить за обновлениями сайта это подписаться на RSS
Если информация была полезной для вас, вы можете поддержать сайт.
Комментарии:
IgorN 21.07.2009 16:55 #
Довольно интересный подход. Как по мне немного запутано для начинающих. Не увидел реализации множественного наследования ролей. Не очень понравилось как вообще представляют роли и ресурсы, почему их не оставить абстрактными понятиями, людям будет тогда легче прикрутить к своему проекту, а так роли - пользователи или группы (что не корректно), ресурсы - страницы, файлы. Роль - это роль, ресурс - это ресурс, зачем пытаться придавать другой смысл словам. Почему бы не вытягивать сразу все роли, ресурсы и привилегии, просто кэшировать их на продакшене и все (код бы стал намного чище и понятнее да и по скоросте шустрее кэшировать).
Ответить
rashad 21.07.2009 18:21 #
ну я так полагаю если вытягивать все роли, даже если кешировать, то может получиться, что в кеше будет довольно здоровый объект, допустим у нас 1000 пользователей и 5000 страниц...
Ответить
IgorN 22.07.2009 08:57 #
Хм... у меня в кэше хранится сам объект ACL. Ресурсов может быть довольно много,хотя я еще не сталкивался с задачей где их 5000 у меня в проекте ресурс это модуль:контроллер :), расширенные варианты пока использовать не приходилось.
Поэтому статья понравилась, идеей подтягивать то, что нужно.
Ответить
андре 03.08.2009 00:33 #
страница index.php выдает ошибку
[Mon Aug  3 01:28:59 2009] [error] PHP Fatal error:  Call to undefined method Zend_Controller_Action_Helper_Redirector::gotoSimple() in /bhome/part3/03/vh35051/pol-mar.ru/www/application/modules/default/controllers/IndexController.php on line 4
как решить проблему?
спасибо
Ответить
Serg 05.09.2009 15:39 #
Может кому интересно.
В текущем виде у меня туториал не запахал. Для того чтобы открывались категории имеющие родителей в файле AclFactory.php
вместо строки $categories = $category->getParents() + array($category); нужно вписать
$categories = $category->getParents();
$categories[] = $category;
В таком виде заработало, иначе перебрасывало на авторизацию.
Ответить
MasKarAl 30.03.2010 15:52 #
Вероятно потому что оператор + для массивов заменяет элементы с одинаковыми ключами.
Примеры тут: http://docs.php.net/manual/en/language.operators.array.php
Ответить
jer 06.09.2009 15:36 #
Понимаю что это перевод и его автор не виноват, но очень мне не понравилась вот эта часть

  private function _accessValid(Zend_Controller_Request_Abstract $request) {
    $user = $request->getParam('user', null);
    // плагин который загружает идентификаторы должен был добавить
    // пользователя в объект запроса, если его там нет значит что то не так
    if($user === null)
      return false;

$request->getParam извлекает данные в порядке установленые_в_коде-get-post-cookie и если этот самый упомятуй в комментарии плагин сделает что-нибудь не так, то введя такой вот запрос http://someurl?user=admin... Если народ потянет этот код без изменений, то тут потенциальная дырень в безопастности.

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

А в остальном статья понравилась, вполне рациональный подход
Ответить
Александр Махомет 06.09.2009 16:04 #
Автор в конце примерно об этом и предупреждает.
Ответить
MasKarAl 30.03.2010 16:01 #
В исходном коде перед плагином AccessCheck вызывается плагин IdentityLoader, в котором такой код: $request->setParam('user', $user); где $user - либо GuestUser, либо Identity из Zend_Auth, поэтому такой трюк врядли получится с подменой в урле юзера..
Ответить
nrnwest 20.01.2010 11:37 #
Только не запускайте проверку прав как рекомендует автор в предыдущих статьях - часть 1 - часть 2. через:
class My_Plugin_Acl extends Zend_Controller_Plugin_Abstract

Если в проекте будете использовать где не будь помощник вида
$this->action() - Zend_View_Helper_Action
то проверка прав не будет произведена для вызываемого действия в помощнике вида action!!!

Ответить
nrnwest 20.01.2010 12:01 #
К стати в этой части также:
class App_Plugin_AccessCheck extends Zend_Controller_Plugin_Abstract {
  public function preDispatch(Zend_Controller_Request_Abstract $request) {


preDispatch не будет запускаться при использовании помощника action !!!

Луче использовать Помощника Action и регистрировать его при старте системы Zend_Controller_Action_Helper_Abstract
Ответить
Алексей 29.06.2010 09:46 #
Можно было бы увидеть исходные коды для скачивания т.к. имеющаяся ссылка битая
Ответить
san 29.06.2010 09:54 #
Обратитесь к автору статьи.
Ответить
Уважаемые пользователи. Комментарии не для того чтобы:
  1. Спрашивать почему у вас не работает код, для этого есть тема форума закрепленная за статьей.
  2. Спрашивать как реализовать ту или иную функциональность, для этого необходимо создать свою тему на форуме.

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

Комментарии имеют древовидную структуру.
Если вы хотите ответить на определенный комментарий - нажмите на ссылку "Ответить" возле этого комментария.

Комментарии не соответствующие этим правилам могут быть удалены. Спасибо за понимание.
Комментарии временно отключены, вы можете воспользоваться форумом.