Контрол на достъпа (оторизация)

Оторизацията определя дали потребителят има достатъчно привилегии, например за достъп до определен ресурс или за извършване на действие. Оторизацията предполага успешно удостоверяване, т.е. че потребителят е влязъл в системата.

Монтаж и изисквания

В примерите ще използваме обект от клас 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);
	}
}
версия: 4.0