Preverjanje dovoljenj (Avtorizacija)
Avtorizacija ugotavlja, ali ima uporabnik zadostna dovoljenja, na primer za dostop do določenega vira ali za izvedbo neke akcije. Avtorizacija predpostavlja predhodno uspešno avtentikacijo, tj. da je uporabnik prijavljen.
V primerih bomo uporabljali objekt razreda Nette\Security\User, ki predstavlja trenutnega
uporabnika in do katerega pridete tako, da si ga pustite predati s pomočjo dependency injection. V presenterjih je dovolj samo
poklicati $user = $this->getUser()
.
Pri zelo preprostih spletnih straneh z administracijo, kjer se ne razlikujejo dovoljenja uporabnikov, je mogoče kot
avtorizacijski kriterij uporabiti že znano metodo isLoggedIn()
. Z drugimi besedami: takoj ko je uporabnik
prijavljen, ima vsa dovoljenja in obratno.
if ($user->isLoggedIn()) { // je uporabnik prijavljen?
deleteItem(); // potem ima dovoljenje za operacijo
}
Vloge
Namen vlog je ponuditi natančnejše upravljanje dovoljenj in ostati neodvisen od uporabniškega imena. Vsakemu uporabniku
takoj ob prijavi dodelimo eno ali več vlog, v katerih bo nastopal. Vloge so lahko preprosti nizi, na primer admin
,
member
, guest
, ipd. Navajajo se kot drugi parameter konstruktorja SimpleIdentity
, bodisi
kot niz ali polje nizov – vlog.
Kot avtorizacijski kriterij bomo zdaj uporabili metodo isInRole()
, ki pove, ali uporabnik nastopa
v dani vlogi:
if ($user->isInRole('admin')) { // je uporabnik v vlogi admina?
deleteItem(); // potem ima dovoljenje za operacijo
}
Kot že veste, se po odjavi uporabnika njegova identiteta ne izbriše nujno. Torej metoda getIdentity()
še naprej
vrača objekt SimpleIdentity
, vključno z vsemi dodeljenimi vlogami. Nette Framework zagovarja načelo »manj kode,
več varnosti«, kjer manj pisanja vodi k bolj varnemu kodu, zato pri ugotavljanju vlog ni treba še preverjati, ali je uporabnik
prijavljen. Metoda isInRole()
dela z efektivnimi vlogami, tj. če je uporabnik prijavljen, izhaja iz vlog,
navedenih v identiteti, če ni prijavljen, ima samodejno posebno vlogo guest
.
Avtorizator
Poleg vlog bomo uvedli še pojma vir in operacija:
- vloga je lastnost uporabnika – npr. moderator, urednik, obiskovalec, registriran uporabnik, skrbnik…
- vir (resource) je nek logični element spletne strani – članek, stran, uporabnik, postavka v meniju, anketa, presenter, …
- operacija (operation) je neka konkretna dejavnost, ki jo uporabnik lahko ali ne more opravljati z virom – na primer izbrisati, urediti, ustvariti, glasovati, …
Avtorizator je objekt, ki odloča, ali ima dana vloga dovoljenje za izvedbo določene operacije z določenim
virom. Gre za objekt, ki implementira vmesnik Nette\Security\Authorizator z edino metodo
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;
}
}
Avtorizator dodamo v konfiguracijo kot storitev DI vsebnika:
services:
- MyAuthorizator
In sledi primer uporabe. Pozor, tokrat kličemo metodo Nette\Security\User::isAllowed()
, ne avtorizatorja, zato
tam ni prvega parametra $role
. Ta metoda kliče MyAuthorizator::isAllowed()
postopoma za vse
uporabnikove vloge in vrača true, če vsaj ena od njih ima dovoljenje.
if ($user->isAllowed('file')) { // lahko uporabnik počne karkoli z virom 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // lahko nad virom 'file' izvede 'delete'?
deleteFile();
}
Oba parametra sta neobvezna, privzeta vrednost null
ima pomen karkoli.
Permission ACL
Nette prihaja z vgrajeno implementacijo avtorizatorja, in sicer razredom Nette\Security\Permission, ki programerju ponuja lahko in fleksibilno ACL (Access Control List) plast za upravljanje dovoljenj in dostopov. Delo z njo temelji na definiciji vlog, virov in posameznih dovoljenj. Pri čemer vloge in viri omogočajo ustvarjanje hierarhij. Za pojasnilo si bomo pokazali primer spletne aplikacije:
guest
: neprijavljen obiskovalec, ki lahko bere in brska po javnem delu spletne strani, tj. bere članke, komentarje in voli v anketahregistered
: prijavljen registriran uporabnik, ki dodatno lahko komentiraadmin
: lahko upravlja članke, komentarje in ankete
Definirali smo si torej določene vloge (guest
, registered
in admin
) in omenili vire
(article
, comment
, poll
), do katerih lahko uporabniki z neko vlogo dostopajo ali izvajajo
določene operacije (view
, vote
, add
, edit
).
Ustvarimo instanco razreda Permission in definiramo vloge. Pri tem lahko izkoristimo t.i. dedovanje vlog, ki zagotovi,
da npr. uporabnik z vlogo administratorja (admin
) lahko dela tudi to, kar navaden obiskovalec spletne strani (in
seveda tudi več).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' deduje od 'guest'
$acl->addRole('admin', 'registered'); // in od njega deduje 'admin'
Zdaj definiramo tudi seznam virov, do katerih lahko uporabniki dostopajo.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Tudi viri lahko uporabljajo dedovanje, mogoče bi bilo na primer vnesti
$acl->addResource('perex', 'article')
.
In zdaj najpomembnejše. Med njimi definiramo pravila, ki določajo, kdo kaj lahko počne s čim:
// najprej nihče ne more početi ničesar
// naj gost lahko pregleduje članke, komentarje in ankete
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// in v anketah dodatno tudi glasuje
$acl->allow('guest', 'poll', 'vote');
// registrirani deduje pravice od gosta, damo mu dodatno pravico komentiranja
$acl->allow('registered', 'comment', 'add');
// administrator lahko pregleduje in ureja karkoli
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Kaj pa, če želimo nekomu preprečiti dostop do določenega vira?
// administrator ne more urejati anket, to bi bilo nedemokratično
$acl->deny('admin', 'poll', 'edit');
Zdaj, ko imamo ustvarjen seznam pravil, lahko preprosto postavljamo avtorizacijska vprašanja:
// lahko gost pregleduje članke?
$acl->isAllowed('guest', 'article', 'view'); // true
// lahko gost ureja članke?
$acl->isAllowed('guest', 'article', 'edit'); // false
// lahko gost glasuje v anketah?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// lahko gost komentira?
$acl->isAllowed('guest', 'comment', 'add'); // false
Enako velja za registriranega uporabnika, ta pa lahko tudi komentira:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Administrator lahko ureja vse, razen anket:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Dovoljenja se lahko tudi dinamično ocenjujejo in odločitev lahko prepustimo lastnemu callbacku, kateremu se predajo vsi parametri:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Kako pa na primer rešiti situacijo, ko ne zadostujejo samo imena vlog in virov, ampak bi želeli definirati, da na primer
vloga registered
lahko ureja vir article
samo, če je njegov avtor? Namesto nizov bomo uporabili
objekte, vloga bo objekt Nette\Security\Role in vir
Nette\Security\Resource. Njihovi metodi
getRoleId()
oz. getResourceId()
bosta vračali prvotne nize:
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';
}
}
In zdaj ustvarimo pravilo:
$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);
In poizvedba na ACL se izvede s predajo objektov:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Vloga lahko deduje od druge vloge ali od več vlog. Kaj pa se zgodi, če ima en prednik akcijo prepovedano in drugi dovoljeno? Kakšne bodo pravice potomca? Določa se glede na težo vloge – zadnja navedena vloga v seznamu prednikov ima največjo težo, prva navedena vloga pa najmanjšo. Bolj nazorno je to iz primera:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// primer A: vloga admin ima manjšo težo kot vloga guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// primer B: vloga admin ima večjo težo kot guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Vloge in vire je mogoče tudi odstraniti (removeRole()
, removeResource()
), mogoče je revertirati
tudi pravila (removeAllow()
, removeDeny()
). Polje vseh neposrednih starševskih vlog vrača
getRoleParents()
, ali dve entiteti dedujeta druga od druge, vračata roleInheritsFrom()
in
resourceInheritsFrom()
.
Dodajanje kot storitve
Naš ustvarjeni ACL si moramo predati v konfiguracijo kot storitev, da ga začne uporabljati objekt $user
, torej
da bo mogoče uporabljati v kodi npr. $user->isAllowed('article', 'view')
. V ta namen si zanj napišemo
tovarno:
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;
}
}
In jo dodamo v konfiguracijo:
services:
- App\Model\AuthorizatorFactory::create
V presenterjih lahko nato preverite dovoljenja na primer v metodi startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}