Проверка на правата (Авторизация)
Авторизацията установява дали потребителят има достатъчно права, например за достъп до определен ресурс или за извършване на някакво действие. Авторизацията предполага предишна успешна автентикация, т.е. че потребителят е влязъл.
В примерите ще използваме обект от клас Nette\Security\User, който представлява
текущия потребител и до който можете да стигнете, като си го поискате
чрез dependency injection. В
презентерите е достатъчно само да извикате $user = $this->getUser()
.
При много прости уеб сайтове с администрация, където не се
разграничават правата на потребителите, е възможно като критерий за
авторизация да се използва вече познатият метод isLoggedIn()
. С други
думи: щом потребителят е влязъл, има всички права и обратно.
if ($user->isLoggedIn()) { // потребителят влязъл ли е?
deleteItem(); // тогава има право на операцията
}
Роля
Смисълът на ролите е да предложат по-точно управление на правата и да
останат независими от потребителското име. На всеки потребител
веднага при влизане приписваме една или повече роли, в които ще
действа. Ролите могат да бъдат прости низове, например admin
,
member
, guest
и т.н. Посочват се като втори параметър на
конструктора SimpleIdentity
, или като низ, или като масив от
низове – роли.
Като критерий за авторизация сега ще използваме метода isInRole()
,
който разкрива дали потребителят действа в дадена роля:
if ($user->isInRole('admin')) { // потребителят в ролята на администратор ли е?
deleteItem(); // тогава има право на операцията
}
Както вече знаете, след излизане на потребителя не е задължително да
се изтрие неговата идентичност. Тоест, методът getIdentity()
продължава да връща обект SimpleIdentity
, включително всички
предоставени роли. Nette Framework изповядва принципа „less code, more security“,
където по-малко писане води до по-сигурен код, затова при установяване
на ролите не е необходимо още да проверявате дали потребителят е
влязъл. Методът 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');
// случай А: ролята 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);
}
}