Контрол на достъпа (оторизация)
Оторизацията определя дали потребителят има достатъчно привилегии, например за достъп до определен ресурс или за извършване на действие. Оторизацията предполага успешно удостоверяване, т.е. че потребителят е влязъл в системата.
В примерите ще използваме обект от клас 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()
, а не методът
authorizer, така че няма първи параметър $role
. Този метод извиква
MyAuthorizator::isAllowed()
последователно за всички потребителски роли и
връща true, ако поне една от тях има разрешение.
ако ($user->isAllowed('file')) { // позволено ли е на потребителя да прави всичко с ресурс 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // позволено ли е на потребителя да изтрие ресурс 'file'?
deleteFile();
}
И двата аргумента са незадължителни и по подразбиране са all.
Разрешения на 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(); // обект Регистриран
$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');
// пример А: ролята admin има по-малка тежест от ролята guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// пример Б: ролята 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);
}
}