Контроль доступа (авторизация)
Авторизация определяет, обладает ли пользователь достаточными привилегиями, например, для доступа к определенному ресурсу или выполнения какого-либо действия. Авторизация предполагает успешную аутентификацию, т.е. что пользователь вошел в систему.
В примерах мы будем использовать объект класса Nette\Security\User, который
представляет текущего пользователя и который вы получаете, передавая
его с помощью инъекции
зависимостей. В презентаторах просто вызывайте
$user = $this->getUser()
.
Для очень простых сайтов с администрированием, где права
пользователей не различаются, можно использовать в качестве критерия
авторизации уже известный метод isLoggedIn()
. Другими словами: как
только пользователь вошел в систему, он имеет права на все действия и
наоборот.
if ($user->isLoggedIn()) { // is user logged in?
deleteItem(); // if so, he may delete an item
}
Роли
Цель ролей – предложить более точное управление правами и
оставаться независимыми от имени пользователя. Как только
пользователь входит в систему, ему назначается одна или несколько
ролей. Сами роли могут быть простыми строками, например, admin
,
member
, guest
и т.д. Они указываются во втором аргументе
конструктора SimpleIdentity
, либо как строка, либо как массив.
В качестве критерия авторизации мы будем использовать метод
isInRole()
, который проверяет, входит ли пользователь в
заданную роль:
if ($user->isInRole('admin')) { // is the admin role assigned to the user?
deleteItem(); // if so, he may delete an item
}
Как вы уже знаете, выход пользователя из системы не стирает его
личность. Таким образом, метод 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')) { // is user allowed to do everything with resource 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // is user allowed to delete a resource 'file'?
deleteFile();
}
Оба аргумента являются необязательными, и их значение по умолчанию означает все.
Разрешение ACL
Nette поставляется со встроенной реализацией авторизатора, классом Nette\Security\Permission, который предлагает легкий и гибкий уровень ACL (Access Control List) для разрешения и контроля доступа. Когда мы работаем с этим классом, мы определяем роли, ресурсы и отдельные разрешения. При этом роли и ресурсы могут образовывать иерархии. Чтобы объяснить это, мы покажем пример веб-приложения:
guest
: посетитель, не вошедший в систему, которому разрешено читать и просматривать публичную часть сайта, т.е. читать статьи, комментировать и голосовать в опросах.registered
: вошедший в систему пользователь, который, помимо этого, может оставлять комментарии.admin
: может управлять статьями, комментариями и опросами
Итак, мы определили определенные роли (guest
, registered
и
admin
) и упомянули ресурсы (article
, comments
, poll
), к
которым пользователи могут получить доступ или предпринять действия
(view
, vote
, add
, edit
).
Мы создаем экземпляр класса Permission и определяем роли. Можно
использовать наследование ролей, что гарантирует, что, например,
пользователь с ролью admin
может делать то, что может делать
обычный посетитель сайта (и, конечно, больше).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' inherits from 'guest'
$acl->addRole('admin', 'registered'); // and 'admin' inherits from 'registered'
Теперь мы определим список ресурсов, к которым пользователи могут получить доступ:
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Ресурсы также могут использовать наследование, например, мы можем
добавить $acl->addResource('perex', 'article')
.
А теперь самое главное. Мы определим между ними правила, определяющие, кто что может делать:
// everything is denied now
// let the guest view articles, comments and polls
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// and also vote in polls
$acl->allow('guest', 'poll', 'vote');
// the registered inherits the permissions from guesta, we will also let him to comment
$acl->allow('registered', 'comment', 'add');
// the administrator can view and edit anything
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Что если мы хотим препятствовать кому-то получить доступ к ресурсу?
// administrator cannot edit polls, that would be undemocractic.
$acl->deny('admin', 'poll', 'edit');
Теперь, когда мы создали набор правил, мы можем просто задавать запросы на авторизацию:
// can guest view articles?
$acl->isAllowed('guest', 'article', 'view'); // true
// can guest edit an article?
$acl->isAllowed('guest', 'article', 'edit'); // false
// can guest vote in polls?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// may guest add comments?
$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
Разрешения также могут оцениваться динамически, и мы можем оставить решение за нашим собственным обратным вызовом, которому передаются все параметры:
$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(); // object Registered
$resource = $acl->getQueriedResource(); // object 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');
// example A: role admin has lower weight than role guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// example B: role admin has greater weight than role 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);
}
}