Nette Documentation Preview

syntax
Управление доступом (авторизация)
*********************************

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

→ [Установка и требования |@home#Установка]

В примерах мы будем использовать объект класса [api:Nette\Security\User], представляющий текущего пользователя; получить его можно, попросив передать его через [внедрение зависимостей |dependency-injection:passing-dependencies]. В презентерах достаточно вызвать `$user = $this->getUser()`.

Для совсем простых сайтов с администрированием, где права пользователей не различаются, в качестве критерия авторизации можно использовать уже знакомый метод `isLoggedIn()`. Иными словами: как только пользователь вошёл, у него есть все права, и наоборот.

```php
if ($user->isLoggedIn()) { // вошёл ли пользователь?
	deleteItem(); // значит, у него есть право на операцию
}
```


Роли
----

Смысл ролей в том, чтобы дать более точное управление правами и оставаться независимыми от имени пользователя. Как только пользователь входит, ему присваивается одна или несколько ролей, в которых он будет действовать. Роли могут быть простыми строками, например `admin`, `member`, `guest` и т. п. Указываются они вторым аргументом конструктора `SimpleIdentity` - строкой или массивом строк, то есть ролей.

В качестве критерия авторизации мы теперь используем метод `isInRole()`, который говорит, действует ли пользователь в данной роли:

```php
if ($user->isInRole('admin')) { // в роли ли admin пользователь?
	deleteItem(); // значит, у него есть право на операцию
}
```

Как вы уже знаете, выход пользователя не обязан удалять его личность. То есть метод `getIdentity()` по-прежнему возвращает объект `SimpleIdentity` со всеми присвоенными ролями. Nette Framework исповедует принцип "меньше кода, больше безопасности", где меньше писанины ведёт к более безопасному коду. Поэтому при проверке ролей вам не нужно проверять, вошёл ли пользователь. Метод `isInRole()` работает с **действующими ролями**: если пользователь вошёл, он исходит из ролей, указанных в личности; если не вошёл, у него автоматически есть особая роль `guest` (или роли [личности гостя |authentication#Личность гостя], если такая предоставлена).


Авторизатор
-----------

Кроме ролей мы введём понятия ресурса и операции:

- **роль** - свойство пользователя, например модератор, редактор, посетитель, зарегистрированный пользователь, администратор...
- **ресурс** - логическая единица сайта: статья, страница, пользователь, пункт меню, опрос, презентер, ...
- **операция** - конкретное действие, которое пользователь может или не может выполнить с ресурсом, например просмотр, редактирование, удаление, голосование, ...

Авторизатор - объект, который решает, есть ли у данной *роли* право выполнить определённую *операцию* с конкретным *ресурсом*. Это объект, реализующий интерфейс [api:Nette\Security\Authorizator] с единственным методом `isAllowed()`:

```php
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;
	}
}
```

Авторизатор мы добавим в конфигурацию [как сервис |dependency-injection:services] DI-контейнера:

```neon
services:
	- MyAuthorizator
```

А вот пример использования. Обратите внимание: на этот раз мы вызываем метод `Nette\Security\User::isAllowed()`, а не метод авторизатора, поэтому первого параметра `$role` нет. Этот метод последовательно вызывает `MyAuthorizator::isAllowed()` для всех ролей пользователя и возвращает true, если право есть хотя бы у одной из них.

```php
if ($user->isAllowed('file')) { // может ли пользователь что-нибудь делать с ресурсом 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // может ли пользователь выполнить 'delete' над ресурсом 'file'?
	deleteFile();
}
```

Оба аргумента необязательны, значение по умолчанию `null` означает *что угодно*.


Permission ACL
--------------

В Nette есть встроенная реализация авторизатора - класс [api:Nette\Security\Permission], дающий программисту лёгкий и гибкий слой ACL (Access Control List) для управления правами и доступом. Работа с ним состоит в определении ролей, ресурсов и отдельных прав. Роли и ресурсы позволяют создавать иерархии. Для пояснения покажем пример веб-приложения:

- `guest`: незарегистрированный посетитель, который может читать и просматривать публичную часть сайта, то есть читать статьи, комментарии и голосовать в опросах.
- `registered`: зарегистрированный вошедший пользователь, который может ещё и писать комментарии.
- `admin`: может управлять статьями, комментариями и опросами.

Мы определили некоторые роли (`guest`, `registered` и `admin`) и упомянули ресурсы (`article`, `comment`, `poll`), к которым пользователи с определённой ролью могут получать доступ или выполнять определённые операции (`view`, `vote`, `add`, `edit`).

Создаём экземпляр класса Permission и определяем **роли**. Можно использовать так называемое наследование ролей, которое обеспечивает, что, например, пользователь с ролью `admin` может делать и то, что может обычный посетитель сайта (и, разумеется, больше).

```php
$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' наследуется от 'guest'
$acl->addRole('admin', 'registered'); // 'admin' наследуется от 'registered'
```

Теперь определим список **ресурсов**, к которым пользователи могут обращаться.

```php
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
```

Ресурсы тоже могут использовать наследование; например, можно было бы указать `$acl->addResource('perex', 'article')`.

А теперь самое важное. Определим между ними правила, задающие, кто что с чем может делать:

```php
// сначала никто не может ничего

// пусть 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']);
```

А что, если мы хотим кому-то **запретить** доступ к определённому ресурсу?

```php
// администратор не может редактировать опросы, это было бы недемократично
$acl->deny('admin', 'poll', 'edit');
```

Теперь, когда набор правил создан, мы можем просто задавать авторизационные вопросы:

```php
// может ли 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
```

То же самое относится к зарегистрированному пользователю, но он может ещё и комментировать:

```php
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
```

Администратор может редактировать всё, кроме опросов:

```php
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
```

Права можно вычислять и динамически, оставив решение собственному callback'у, которому передаются все параметры:

```php
$assertion = function (Permission $acl, ?string $role, ?string $resource, ?string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);
```

А как справиться с ситуацией, когда одних имён ролей и ресурсов мало и нам хотелось бы определить, что, например, роль `registered` может редактировать ресурс `article`, только если она его автор? Мы воспользуемся объектами вместо строк: ролью будет объект [api:Nette\Security\Role], а ресурсом - объект [api:Nette\Security\Resource]. Их методы `getRoleId()` и соответственно `getResourceId()` вернут исходные строки:

```php
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';
	}
}
```

А теперь создадим правило:

```php
$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 выполняется передачей объектов:

```php
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
```

Роль может наследоваться от одной роли или от нескольких. Но что произойдёт, если у одного предка действие запрещено, а у другого разрешено? Какими будут права потомка? Это определяется весом роли: последняя роль в списке предков имеет наибольший вес, первая - наименьший. Из примера это виднее:

```php
$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')`. Для этого напишем для него фабрику:

```php
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;
	}
}
```

И добавим её в конфигурацию:

```neon
services:
	- App\Model\AuthorizatorFactory::create
```

В презентерах вы затем можете проверять права, например в методе `startup()`:

```php
protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
```

Управление доступом (авторизация)

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

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

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