Контроль доступу (авторизація)

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

→ Встановлення та вимоги

У прикладах ми будемо використовувати об'єкт класу Nette\Security\User, який представляє поточного користувача і який ви отримуєте, передаючи його за допомогою ін'єкції залежностей. У презентаторах просто викликайте $user = $this->getUser().

Для дуже простих сайтів з адмініструванням, де права користувачів не різняться, можна використовувати як критерій авторизації вже відомий метод isLoggedIn(). Інакше кажучи: щойно користувач увійшов у систему, він має права на всі дії і навпаки.

if ($user->isLoggedIn()) { // чи користувач залогінений?
	deleteItem(); // якщо так, то він може видалити елемент
}

Ролі

Мета ролей – запропонувати більш точне управління правами і залишатися незалежними від імені користувача. Щойно користувач входить у систему, йому призначається одна або кілька ролей. Самі ролі можуть бути простими рядками, наприклад, admin, member, guest тощо. Вони вказуються в другому аргументі конструктора SimpleIdentity, або як рядок, або як масив.

Як критерій авторизації ми будемо використовувати метод isInRole(), який перевіряє, чи входить користувач у задану роль:

if ($user->isInRole('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')) { // чи дозволено користувачу видаляти ресурс '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' успадковує від 'guest'
$acl->addRole('admin', 'registered'); // і 'admin' успадковує від 'registered'

Тепер ми визначимо список ресурсів, до яких користувачі можуть отримати доступ:

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

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

А тепер найголовніше. Ми визначимо між ними правила, які визначають, хто що може робити:

// все заперечується тепер

// дозволити гостю переглядати статті, коментарі та опитування
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// а також голосувати в опитуваннях
$acl->allow('guest', 'poll', 'vote');

// зареєстрований успадковує права від guesta, йому також дозволимо коментувати
$acl->allow('registered', 'comment', 'add');

// адміністратор може переглядати та редагувати будь-що
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Що якщо ми хочемо перешкодити комусь отримати доступ до ресурсу?

// адміністратор не може редагувати опитування, це було б недемократично.
$acl->deny('admin', 'poll', 'edit');

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

// чи може гість переглядати статті?
$acl->isAllowed('guest', 'article', 'view'); // true

// чи може гість редагувати статтю?
$acl->isAllowed('guest', 'article', 'edit'); // false

// чи може гість голосувати в опитуваннях?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// чи може гість додавати коментарі?
$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(); // об'єкт 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