Проверка разрешений (Авторизация)

Авторизация определяет, имеет ли пользователь достаточные разрешения, например, для доступа к определенному ресурсу или для выполнения какого-либо действия. Авторизация предполагает предыдущую успешную аутентификацию, т.е. что пользователь вошел в систему.

Установка и требования

В примерах мы будем использовать объект класса 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);
	}
}
версия: 4.0