Управление доступом (авторизация)
Авторизация проверяет, есть ли у пользователя достаточные права, например для доступа к определённому ресурсу или для выполнения действия. Авторизация предполагает предшествующую успешную аутентификацию, то есть что пользователь вошёл.
В примерах мы будем использовать объект класса Nette\Security\User, представляющий
текущего пользователя; получить его можно, попросив передать его через
внедрение зависимостей. В презентерах
достаточно вызвать $user = $this->getUser().
Для совсем простых сайтов с администрированием, где права
пользователей не различаются, в качестве критерия авторизации можно
использовать уже знакомый метод isLoggedIn(). Иными словами: как
только пользователь вошёл, у него есть все права, и наоборот.
if ($user->isLoggedIn()) { // вошёл ли пользователь?
deleteItem(); // значит, у него есть право на операцию
}
Роли
Смысл ролей в том, чтобы дать более точное управление правами и
оставаться независимыми от имени пользователя. Как только
пользователь входит, ему присваивается одна или несколько ролей, в
которых он будет действовать. Роли могут быть простыми строками,
например admin, member, guest и т. п. Указываются они вторым
аргументом конструктора SimpleIdentity – строкой или массивом строк,
то есть ролей.
В качестве критерия авторизации мы теперь используем метод
isInRole(), который говорит, действует ли пользователь в
данной роли:
if ($user->isInRole('admin')) { // в роли ли admin пользователь?
deleteItem(); // значит, у него есть право на операцию
}
Как вы уже знаете, выход пользователя не обязан удалять его личность.
То есть метод getIdentity() по-прежнему возвращает объект
SimpleIdentity со всеми присвоенными ролями. Nette Framework исповедует
принцип „меньше кода, больше безопасности“, где меньше писанины ведёт
к более безопасному коду. Поэтому при проверке ролей вам не нужно
проверять, вошёл ли пользователь. Метод isInRole() работает с
действующими ролями: если пользователь вошёл, он исходит из ролей,
указанных в личности; если не вошёл, у него автоматически есть особая
роль guest (или роли личности гостя,
если такая предоставлена).
Авторизатор
Кроме ролей мы введём понятия ресурса и операции:
- роль – свойство пользователя, например модератор, редактор, посетитель, зарегистрированный пользователь, администратор…
- ресурс – логическая единица сайта: статья, страница, пользователь, пункт меню, опрос, презентер, …
- операция – конкретное действие, которое пользователь может или не может выполнить с ресурсом, например просмотр, редактирование, удаление, голосование, …
Авторизатор – объект, который решает, есть ли у данной роли право
выполнить определённую операцию с конкретным ресурсом. Это
объект, реализующий интерфейс 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')) { // может ли пользователь выполнить 'delete' над ресурсом 'file'?
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' наследуется от 'registered'
Теперь определим список ресурсов, к которым пользователи могут обращаться.
$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');
// registered наследует права от 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(). Существование роли или ресурса проверяют
hasRole() и hasResource(), списки всех зарегистрированных ролей и
ресурсов возвращают getRoles() и getResources(), а всё целиком можно
стереть через removeAllRoles() и removeAllResources().
Добавление в качестве сервиса
Созданный нами 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);
}
}