Контроль доступу (авторизація)
Авторизація визначає, чи володіє користувач достатніми привілеями, наприклад, для доступу до певного ресурсу або виконання будь-якої дії. Авторизація передбачає успішну аутентифікацію, тобто що користувач увійшов у систему.
→ Встановлення та вимоги
У прикладах ми будемо використовувати об'єкт класу 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);
}
}