Проверка разрешений (Авторизация)
Авторизация определяет, имеет ли пользователь достаточные разрешения, например, для доступа к определенному ресурсу или для выполнения какого-либо действия. Авторизация предполагает предыдущую успешную аутентификацию, т.е. что пользователь вошел в систему.
В примерах мы будем использовать объект класса Nette\Security\User, который
представляет текущего пользователя и к которому вы можете получить
доступ, запросив его передачу с помощью dependency injection. В презентерах
достаточно просто вызвать $user = $this->getUser()
.
Для очень простых веб-сайтов с администрированием, где не
различаются права пользователей, в качестве критерия авторизации
можно использовать уже известный метод isLoggedIn()
. Другими словами:
как только пользователь вошел в систему, он имеет все права, и
наоборот.
if ($user->isLoggedIn()) { // пользователь вошел в систему?
deleteItem(); // тогда у него есть права на операцию
}
Роли
Смысл ролей в том, чтобы предложить более точное управление
разрешениями и оставаться независимым от имени пользователя. Каждому
пользователю сразу при входе присваивается одна или несколько ролей, в
которых он будет выступать. Роли могут быть простыми строками,
например, admin
, member
, guest
и т.д. Они указываются как
второй параметр конструктора SimpleIdentity
, либо как строка, либо как
массив строк – ролей.
В качестве критерия авторизации теперь используем метод
isInRole()
, который сообщает, выступает ли пользователь в
данной роли:
if ($user->isInRole('admin')) { // пользователь в роли администратора?
deleteItem(); // тогда у него есть права на операцию
}
Как вы уже знаете, после выхода пользователя из системы его
идентификатор может не удаляться. То есть метод getIdentity()
по-прежнему возвращает объект SimpleIdentity
, включая все
предоставленные роли. Nette Framework придерживается принципа «less code, more
security», когда меньше писанины ведет к более безопасному коду, поэтому
при проверке ролей вам не нужно дополнительно проверять, вошел ли
пользователь в систему. Метод isInRole()
работает с эффективными
ролями, т.е. если пользователь вошел в систему, он исходит из ролей,
указанных в идентификаторе, если не вошел, у него автоматически
специальная роль guest
.
Авторизатор
Кроме ролей, введем еще понятия ресурс и операция:
- роль – это свойство пользователя – например, модератор, редактор, посетитель, зарегистрированный пользователь, администратор…
- ресурс (resource) – это какой-то логический элемент веб-сайта – статья, страница, пользователь, пункт меню, опрос, презентер, …
- операция (operation) – это какое-то конкретное действие, которое пользователь может или не может делать с ресурсом – например, удалить, изменить, создать, голосовать, …
Авторизатор — это объект, который решает, имеет ли данная роль
разрешение выполнить определенную операцию с определенным
ресурсом. Это объект, реализующий интерфейс Nette\Security\Authorizator с
единственным методом isAllowed()
:
class MyAuthorizator implements Nette\Security\Authorizator
{
public function isAllowed($role, $resource, $operation): bool
{
if ($role === 'admin') {
return true;
}
if ($role === 'user' && $resource === 'article') {
return true;
}
// ...
return false;
}
}
Авторизатор добавим в конфигурацию как сервис DI-контейнера:
services:
- MyAuthorizator
А далее пример использования. Внимание, на этот раз мы вызываем метод
Nette\Security\User::isAllowed()
, а не авторизатор, поэтому там нет первого
параметра $role
. Этот метод вызывает MyAuthorizator::isAllowed()
последовательно для всех ролей пользователя и возвращает true, если хотя
бы одна из них имеет разрешение.
if ($user->isAllowed('file')) { // может ли пользователь делать что-либо с ресурсом 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // может ли над ресурсом 'file' выполнить 'delete'?
deleteFile();
}
Оба параметра являются необязательными, значение по умолчанию
null
означает что угодно.
Permission ACL
Nette поставляется со встроенной реализацией авторизатора, а именно классом Nette\Security\Permission, предоставляющим программисту легкий и гибкий слой ACL (Access Control List) для управления разрешениями и доступом. Работа с ним заключается в определении ролей, ресурсов и отдельных разрешений. При этом роли и ресурсы позволяют создавать иерархии. Для пояснения покажем пример веб-приложения:
guest
: не вошедший посетитель, который может читать и просматривать публичную часть веб-сайта, т.е. читать статьи, комментарии и голосовать в опросахregistered
: вошедший зарегистрированный пользователь, который дополнительно может комментироватьadmin
: может управлять статьями, комментариями и опросами
Мы определили некоторые роли (guest
, registered
и admin
) и
упомянули ресурсы (article
, comment
, poll
), к которым
пользователи с какой-либо ролью могут получать доступ или выполнять
определенные операции (view
, vote
, add
, edit
).
Создадим экземпляр класса Permission и определим роли. При этом можно
использовать так называемое наследование ролей, которое обеспечит,
что, например, пользователь с ролью администратора (admin
) может
делать и то, что обычный посетитель веб-сайта (и, конечно, больше).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' наследует от 'guest'
$acl->addRole('admin', 'registered'); // а от него наследует 'admin'
Теперь определим и список ресурсов, к которым пользователи могут получать доступ.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Ресурсы также могут использовать наследование, можно было бы,
например, указать $acl->addResource('perex', 'article')
.
А теперь самое важное. Определим между ними правила, определяющие, кто что может с чем делать:
// сначала никто ничего не может делать
// пусть guest может просматривать статьи, комментарии и опросы
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// а в опросах дополнительно и голосовать
$acl->allow('guest', 'poll', 'vote');
// зарегистрированный наследует права от guest, дадим ему дополнительно право комментировать
$acl->allow('registered', 'comment', 'add');
// администратор может просматривать и редактировать что угодно
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Что если мы хотим кому-то запретить доступ к определенному ресурсу?
// администратор не может редактировать опросы, это было бы недемократично
$acl->deny('admin', 'poll', 'edit');
Теперь, когда у нас создан список правил, мы можем просто задавать авторизационные запросы:
// может ли guest просматривать статьи?
$acl->isAllowed('guest', 'article', 'view'); // true
// может ли guest редактировать статьи?
$acl->isAllowed('guest', 'article', 'edit'); // false
// может ли guest голосовать в опросах?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// может ли guest комментировать?
$acl->isAllowed('guest', 'comment', 'add'); // false
То же самое относится и к зарегистрированному пользователю, однако он может и комментировать:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Администратор может редактировать все, кроме опросов:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Разрешения также могут оцениваться динамически, и мы можем оставить решение на собственном обратном вызове (callback), которому передаются все параметры:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Как же, например, решить ситуацию, когда недостаточно только названий
ролей и ресурсов, но мы хотели бы определить, что, например, роль
registered
может редактировать ресурс article
только если
является его автором? Вместо строк используем объекты, роль будет
объектом Nette\Security\Role, а
ресурс — объектом Nette\Security\Resource. Их методы
getRoleId()
соответственно getResourceId()
будут возвращать
исходные строки:
class Registered implements Nette\Security\Role
{
public $id;
public function getRoleId(): string
{
return 'registered';
}
}
class Article implements Nette\Security\Resource
{
public $authorId;
public function getResourceId(): string
{
return 'article';
}
}
А теперь создадим правило:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // объект Registered
$resource = $acl->getQueriedResource(); // объект Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
И запрос к ACL выполняется передачей объектов:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Роль может наследоваться от другой роли или от нескольких ролей. Но что произойдет, если у одного предка действие запрещено, а у другого разрешено? Какими будут права потомка? Это определяется весом роли – последняя указанная роль в списке предков имеет наибольший вес, первая указанная роль – наименьший. Нагляднее это видно из примера:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// случай A: роль admin имеет меньший вес, чем роль guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// случай B: роль admin имеет больший вес, чем guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Роли и ресурсы можно также удалять (removeRole()
, removeResource()
),
можно отменять и правила (removeAllow()
, removeDeny()
). Массив всех
прямых родительских ролей возвращает getRoleParents()
, наследуют ли
две сущности друг от друга, возвращают roleInheritsFrom()
и
resourceInheritsFrom()
.
Добавление как сервиса
Нами созданный ACL нужно передать в конфигурацию как сервис, чтобы его
начал использовать объект $user
, то есть чтобы можно было
использовать в коде, например, $user->isAllowed('article', 'view')
. Для этой
цели напишем для него фабрику:
namespace App\Model;
class AuthorizatorFactory
{
public static function create(): Nette\Security\Permission
{
$acl = new Nette\Security\Permission;
$acl->addRole(/* ... */);
$acl->addResource(/* ... */);
$acl->allow(/* ... */);
return $acl;
}
}
И добавим ее в конфигурацию:
services:
- App\Model\AuthorizatorFactory::create
В презентерах затем можно проверить разрешения, например, в методе
startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}