Ověřování oprávnění (Autorizace)

Autorizace zjišťuje, zda má uživatel dostatečná oprávnění například pro přístup k určitému zdroji či pro provedení nějaké akce. Autorizace předpokládá předchozí úspěšnou autentizaci, tj. že uživatel je přihlášen.

Instalace a požadavky

V příkladech budeme používat objekt třídy Nette\Security\User, který představuje aktuálního uživatele a ke kterému se dostanete tak, že si jej necháte předat pomocí dependency injection. V presenterech stačí jen zavolat $user = $this->getUser().

U velmi jednoduchých webů s administrací, kde se nerozlišují oprávnění uživatelů, je možné jako autorizační kritérium použít již známou metodu isLoggedIn(). Jinými slovy: jakmile je uživatel přihlášen, má veškerá oprávnění a naopak.

if ($user->isLoggedIn()) { // je uživatel přihlášen?
	deleteItem(); // pak má k operaci oprávnění
}

Role

Smyslem rolí je nabídnout přesnější řízení oprávnění a zůstat nezávislý na uživatelském jméně. Každému uživateli hned při přihlášení přiřkneme jednu či více rolí, ve kterých bude vystupovat. Role mohou být jednoduché řetězce například admin, member, guest, apod. Uvádí se jako druhý parametr konstruktoru SimpleIdentity, buď jako řetězec nebo pole řetězců – rolí.

Jako autorizační kritérium nyní použijeme metodu isInRole(), která prozradí, zda uživatel vystupuje v dané roli:

if ($user->isInRole('admin')) { // je uživatel v roli admina?
	deleteItem(); // pak má k operaci oprávnění
}

Jak už víte, po odhlášení uživatele se nemusí smazat jeho identita. Tedy i nadále metoda getIdentity() vrací objekt SimpleIdentity, včetně všech udělených rolí. Nette Framework vyznává princip „less code, more security“, kdy méně psaní vede k více zabezpečenému kódu, proto při zjišťování rolí nemusíte ještě ověřovat, zda je uživatel přihlášený. Metoda isInRole() pracuje s efektivními rolemi, tj. pokud je uživatel přihlášen, vychází z rolí uvedených v identitě, pokud přihlášen není, má automaticky speciální roli guest.

Autorizátor

Kromě rolí zavedeme ještě pojmy zdroj a operace:

  • role je vlastnost uživatele – např. moderátor, redaktor, návštěvník, zaregistrovaný uživatel, správce…
  • zdroj (resource) je nějaký logický prvek webu – článek, stránka, uživatel, položka v menu, anketa, presenter, …
  • operace (operation) je nějaká konkrétní činnost, kterou uživatel může či nemůže se zdrojem dělat – například smazat, upravit, vytvořit, hlasovat, …

Autorizátor je objekt, který rozhoduje, zda má daná role povolení provést určitou operaci s určitým zdrojem. Jde o objekt implementující rozhraní Nette\Security\Authorizator s jedinou metodu 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;
	}
}

Autorizátor přidáme do konfigurace jako službu DI kontejneru:

services:
	- MyAuthorizator

A následuje příklad použití. Pozor, tentokrát voláme metodu Nette\Security\User::isAllowed(), nikoliv autorizátor, takže tam není první parametr $role. Tato metoda volá MyAuthorizator::isAllowed() postupně pro všechny uživatelovy role a vrací true, pokud alespoň jedna z nich má povolení.

if ($user->isAllowed('file')) { // může uživatel dělat cokoliv se zdrojem 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // může nad zdrojem 'file' provést 'delete'?
	deleteFile();
}

Oba parametry jsou volitelné, výchozí hodnota null má význam cokoliv.

Permission ACL

Nette přichází s vestavěnou implementací autorizátoru, a to třídou Nette\Security\Permission poskytující programátorovi lehkou a flexibilní ACL (Access Control List) vrstvu pro řízení oprávnění a přístupů. Práce s ní spočívá v definici rolí, zdrojů a jednotlivých oprávnění. Přičemž role a zdroje umožňují vytvářet hierarchie. Na vysvětlenou si ukážeme příklad webové aplikace:

  • guest: nepřihlášený návštěvník, který může číst a procházet veřejnou část webu, tzn. číst články, komentáře a volit v anketách
  • registered: přihlášený registrovaný uživatel, který navíc může komentovat
  • admin: může spravovat články, komentáře i ankety

Nadefinovali jsme si tedy určité role (guest, registered a admin) a zmínili zdroje (article, comment, poll), ke kterým mohou uživatelé s nějakou rolí přistupovat nebo provádět určité operace (view, vote, add, edit).

Vytvoříme instanci třídy Permission a nadefinujeme role. Lze přitom využít tzv. dědičnost rolí, která zajistí, že např. uživatel s rolí administrátora (admin) může dělat i to co obyčejný návštěvník webu (a samozřejmě i více).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' dědí od 'guest'
$acl->addRole('admin', 'registered'); // a od něj dědí 'admin'

Nyní nadefinujeme i seznam zdrojů, ke kterým mohou uživatelé přistupovat.

$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');

I zdroje mohou používat dědičnost, bylo by možné například zadat $acl->addResource('perex', 'article').

A teď to nejdůležitější. Nadefinujeme mezi nimi pravidla určující, kdo co může s čím dělat:

// nejprve nikdo nemůže dělat nic

// nechť guest může prohlížet články, komentáře i ankety
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// a v anketách navíc i hlasovat
$acl->allow('guest', 'poll', 'vote');

// registrovaný dědí práva od guesta, dáme mu navíc právo komentovat
$acl->allow('registered', 'comment', 'add');

// administrátor může prohlížet a editovat cokoliv
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

Co když chceme někomu zamezit k určitému zdroji přístup?

// administrátor nemůže editovat ankety, to by bylo nedemokratické
$acl->deny('admin', 'poll', 'edit');

Nyní, když máme vytvořený seznam pravidel, můžeme jednoduše klást autorizační dotazy:

// může guest prohlížet články?
$acl->isAllowed('guest', 'article', 'view'); // true

// může guest editovat články?
$acl->isAllowed('guest', 'article', 'edit'); // false

// může guest hlasovat v anketách?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// může guest komentovat?
$acl->isAllowed('guest', 'comment', 'add'); // false

Totéž platí pro registrovaného uživatele, ten však může i komentovat:

$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false

Administrátor může editovat vše, kromě anket:

$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true

Oprávění mohou také být vyhodnocována dynamicky a můžeme rozhodnutí nechat na vlastním callbacku, kterému se předají všechny parametry:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	return /* ... */;
};

$acl->allow('registered', 'comment', null, $assertion);

Jak ale třeba řešit situaci, kdy nestačí jen názvy rolí a zdrojů, ale chtěli bychom definovat, že třeba role registered může editovat zdroj article jen pokud je jeho autorem? Místo řetězců použijeme objekty, role bude objekt Nette\Security\Role a zdroj Nette\Security\Resource. Jejich metody getRoleId() resp. getResourceId() budou vracet původní řetezce:

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';
	}
}

A nyní vytvoříme pravidlo:

$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
	$role = $acl->getQueriedRole(); // objekt Registered
	$resource = $acl->getQueriedResource(); // objekt Article
	return $role->id === $resource->authorId;
};

$acl->allow('registered', 'article', 'edit', $assertion);

A dotaz na ACL se provede předáním objektů:

$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');

Role může dědit od jiné role či od více rolí. Co se ale stane, pokud má jeden předek akci zakázanou a druhý povolenou? Jaké budou práva potomka? Určuje se to podle váhy role – poslední uvedená role v seznamu předků má největší váhu, první uvedená role tu nejmenší. Více názorné je to z příkladu:

$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');

$acl->addResource('backend');

$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');

// případ A: role admin má menší váhu než role guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// případ B: role admin má větší váhu než guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Role a zdroje lze i odebírat (removeRole(), removeResource()), lze revertovat i pravidla (removeAllow(), removeDeny()). Pole všech přímých rodičovských rolí vrací getRoleParents(), zda od sebe dvě entity dědí vrací roleInheritsFrom() a resourceInheritsFrom().

Přidání jako služby

Námi vytvořené ACL si potřebujeme předat do konfigurace jako službu, aby jej začal používat objekt $user, tedy aby bylo možné používat v kódu např. $user->isAllowed('article', 'view'). Za tím účelem si na něj napíšeme továrnu:

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;
	}
}

A přidáme ji do konfigurace:

services:
	- App\Model\AuthorizatorFactory::create

V presenterech pak můžete ověřit oprávnění například v metodě startup():

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isAllowed('backend')) {
		$this->error('Forbidden', 403);
	}
}
verze: 4.0 3.x 2.x