Перевірка прав доступу (Авторизація)

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

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

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

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

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

Ролі

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

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

if ($user->isInRole('admin')) { // чи користувач у ролі адміністратора?
	deleteItem(); // тоді він має право на операцію
}

Як ви вже знаєте, після виходу користувача його ідентичність може не видалятися. Тобто метод getIdentity() і надалі повертає об'єкт SimpleIdentity, включно з усіма наданими ролями. Nette Framework дотримується принципу “менше коду, більше безпеки”, коли менше писанини призводить до більш безпечного коду, тому при перевірці ролей вам не потрібно додатково перевіряти, чи користувач залогінений. Метод isInRole() працює з ефективними ролями, тобто якщо користувач залогінений, він базується на ролях, зазначених в ідентичності, якщо не залогінений, він автоматично має спеціальну роль guest.

Авторизатор

Крім ролей, ми введемо ще поняття ресурсу та операції:

  • роль — це властивість користувача – напр. модератор, редактор, відвідувач, зареєстрований користувач, адміністратор…
  • ресурс (resource) — це якийсь логічний елемент веб-сайту – стаття, сторінка, користувач, елемент меню, опитування, presenter, …
  • операція (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

У presenter'ах потім можна перевіряти права доступу, наприклад, у методі startup():

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
версія: 4.0