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.

Instalare și cerințe

Î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 sondaje
  • registered: utilizator înregistrat conectat, care în plus poate comenta
  • admin: 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);
	}
}
versiune: 4.0