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.
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áchregistered
: přihlášený registrovaný uživatel, který navíc může komentovatadmin
: 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);
}
}