Verificarea permisiunilor (Autorizare)
Autorizarea verifică dacă un utilizator are permisiuni suficiente, de exemplu, pentru a accesa o anumită resursă sau pentru a efectua o anumită acțiune. Autorizarea presupune autentificarea prealabilă reușită, adică utilizatorul este conectat.
În exemple vom folosi obiectul clasei Nette\Security\User, care reprezintă utilizatorul
curent și la care ajungeți solicitându-l prin injecția de dependențe. În presenteri este
suficient doar să apelați $user = $this->getUser()
.
Pentru site-uri web foarte simple cu administrare, unde permisiunile utilizatorilor nu sunt diferențiate, se poate folosi ca
și criteriu de autorizare metoda deja cunoscută isLoggedIn()
. Cu alte cuvinte: odată ce utilizatorul este
conectat, are toate permisiunile și invers.
if ($user->isLoggedIn()) { // este utilizatorul conectat?
deleteItem(); // atunci are permisiunea pentru operație
}
Roluri
Scopul rolurilor este de a oferi un control mai precis al permisiunilor și de a rămâne independent de numele de utilizator.
Fiecărui utilizator, imediat după autentificare, i se atribuie unul sau mai multe roluri în care va acționa. Rolurile pot fi
șiruri simple, de exemplu admin
, member
, guest
, etc. Se specifică ca al doilea parametru
al constructorului SimpleIdentity
, fie ca șir, fie ca array de șiruri – roluri.
Ca și criteriu de autorizare vom folosi acum metoda isInRole()
, care indică dacă utilizatorul acționează în
rolul respectiv:
if ($user->isInRole('admin')) { // este utilizatorul în rolul de admin?
deleteItem(); // atunci are permisiunea pentru operație
}
După cum știți deja, după deconectarea utilizatorului, identitatea sa nu trebuie neapărat ștearsă. Adică metoda
getIdentity()
returnează în continuare obiectul SimpleIdentity
, inclusiv toate rolurile acordate.
Nette Framework adoptă principiul „less code, more security”, unde mai puțin scris duce la un cod mai sigur, de aceea la
verificarea rolurilor nu trebuie să verificați și dacă utilizatorul este conectat. Metoda isInRole()
lucrează cu
roluri efective, adică dacă utilizatorul este conectat, se bazează pe rolurile specificate în identitate, dacă nu este
conectat, are automat rolul special guest
.
Autorizator
Pe lângă roluri, vom introduce și conceptele de resursă și operație:
- rol este o proprietate a utilizatorului – de ex. moderator, redactor, vizitator, utilizator înregistrat, administrator…
- resursă (resource) este un element logic al site-ului – articol, pagină, utilizator, element de meniu, sondaj, presenter, …
- operație (operation) este o activitate specifică pe care utilizatorul o poate sau nu o poate face cu resursa – de exemplu, șterge, edita, crea, vota, …
Autorizatorul este un obiect care decide dacă rolul dat are permisiunea de a efectua o anumită operație cu
o anumită resursă. Este un obiect care implementează interfața Nette\Security\Authorizator cu o singură
metodă 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;
}
}
Adăugăm autorizatorul în configurație ca serviciu al containerului DI:
services:
- MyAuthorizator
Și urmează exemplul de utilizare. Atenție, de data aceasta apelăm metoda Nette\Security\User::isAllowed()
, nu
autorizatorul, deci nu există primul parametru $role
. Această metodă apelează
MyAuthorizator::isAllowed()
succesiv pentru toate rolurile utilizatorului și returnează true dacă cel puțin unul
dintre ele are permisiunea.
if ($user->isAllowed('file')) { // poate utilizatorul să facă orice cu resursa 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // poate efectua 'delete' asupra resursei 'file'?
deleteFile();
}
Ambii parametri sunt opționali, valoarea implicită null
are semnificația orice.
Permission ACL
Nette vine cu o implementare încorporată a autorizatorului, și anume clasa Nette\Security\Permission, care oferă programatorului un strat ACL (Access Control List) ușor și flexibil pentru gestionarea permisiunilor și accesului. Lucrul cu aceasta constă în definirea rolurilor, resurselor și permisiunilor individuale. Rolurile și resursele permit crearea de ierarhii. Pentru a explica, vom arăta un exemplu de aplicație web:
guest
: vizitator neconectat, care poate citi și naviga partea publică a site-ului, adică citi articole, comentarii și vota în sondajeregistered
: utilizator înregistrat conectat, care în plus poate comentaadmin
: poate administra articole, comentarii și sondaje
Am definit deci anumite roluri (guest
, registered
și admin
) și am menționat resurse
(article
, comment
, poll
), la care utilizatorii cu un anumit rol pot accesa sau efectua
anumite operații (view
, vote
, add
, edit
).
Creăm o instanță a clasei Permission și definim rolurile. Se poate utiliza așa-numita moștenire a rolurilor, care
asigură că, de exemplu, un utilizator cu rolul de administrator (admin
) poate face și ceea ce poate face un
vizitator obișnuit al site-ului (și, desigur, mai mult).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' moștenește de la 'guest'
$acl->addRole('admin', 'registered'); // și de la el moștenește 'admin'
Acum definim și lista de resurse, la care utilizatorii pot accesa.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Și resursele pot folosi moștenirea, ar fi posibil, de exemplu, să specificăm
$acl->addResource('perex', 'article')
.
Și acum cel mai important lucru. Definim între ele regulile care determină cine ce poate face cu ce:
// la început, nimeni nu poate face nimic
// permite guest să vizualizeze articole, comentarii și sondaje
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// și în sondaje, în plus, să voteze
$acl->allow('guest', 'poll', 'vote');
// registered moștenește drepturile de la guest, îi dăm în plus dreptul de a comenta
$acl->allow('registered', 'comment', 'add');
// administratorul poate vizualiza și edita orice
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Ce se întâmplă dacă vrem să interzicem cuiva accesul la o anumită resursă?
// administratorul nu poate edita sondaje, ar fi nedemocratic
$acl->deny('admin', 'poll', 'edit');
Acum, când avem creată lista de reguli, putem pune simplu întrebări de autorizare:
// poate guest să vizualizeze articole?
$acl->isAllowed('guest', 'article', 'view'); // true
// poate guest să editeze articole?
$acl->isAllowed('guest', 'article', 'edit'); // false
// poate guest să voteze în sondaje?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// poate guest să comenteze?
$acl->isAllowed('guest', 'comment', 'add'); // false
Același lucru este valabil și pentru utilizatorul înregistrat, însă acesta poate și comenta:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Administratorul poate edita totul, cu excepția sondajelor:
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Permisiunile pot fi evaluate și dinamic și putem lăsa decizia pe seama unui callback propriu, căruia i se transmit toți parametrii:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Dar cum să rezolvăm, de exemplu, situația în care nu sunt suficiente doar numele rolurilor și resurselor, ci am dori să
definim că, de exemplu, rolul registered
poate edita resursa article
doar dacă este autorul său? În
loc de șiruri, vom folosi obiecte, rolul va fi obiectul Nette\Security\Role și resursa Nette\Security\Resource. Metodele lor
getRoleId()
respectiv getResourceId()
vor returna șirurile originale:
class Registered implements Nette\Security\Role
{
public $id;
public function getRoleId(): string
{
return 'registered';
}
}
class Article implements Resource
{
public $authorId;
public function getResourceId(): string
{
return 'article';
}
}
Și acum creăm regula:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // obiectul Registered
$resource = $acl->getQueriedResource(); // obiectul Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
Și interogarea ACL se efectuează prin transmiterea obiectelor:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Un rol poate moșteni de la alt rol sau de la mai multe roluri. Dar ce se întâmplă dacă un părinte are acțiunea interzisă și altul permisă? Care vor fi drepturile descendentului? Se determină după greutatea rolului – ultimul rol specificat în lista părinților are cea mai mare greutate, primul rol specificat are cea mai mică. Este mai clar din exemplu:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// cazul A: rolul admin are o greutate mai mică decât rolul guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// cazul B: rolul admin are o greutate mai mare decât guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Rolurile și resursele pot fi și eliminate (removeRole()
, removeResource()
), pot fi anulate și
regulile (removeAllow()
, removeDeny()
). Array-ul tuturor rolurilor părinte directe este returnat de
getRoleParents()
, dacă două entități moștenesc una de la alta este returnat de roleInheritsFrom()
și resourceInheritsFrom()
.
Adăugarea ca servicii
ACL-ul creat de noi trebuie să îl transmitem configurației ca serviciu, pentru ca obiectul $user
să înceapă
să îl folosească, adică să fie posibil să folosim în cod, de exemplu, $user->isAllowed('article', 'view')
.
În acest scop, vom scrie o fabrică pentru el:
namespace App\Model;
class AuthorizatorFactory
{
public static function create(): Nette\Security\Permission
{
$acl = new Permission;
$acl->addRole(/* ... */);
$acl->addResource(/* ... */);
$acl->allow(/* ... */);
return $acl;
}
}
Și o adăugăm în configurație:
services:
- App\Model\AuthorizatorFactory::create
În presenteri puteți apoi verifica permisiunile, de exemplu, în metoda startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}