Controllo degli accessi (autorizzazione)

L'autorizzazione verifica se un utente ha permessi sufficienti, per esempio per accedere a una determinata risorsa o per eseguire un'azione. L'autorizzazione presuppone che l'autenticazione sia già avvenuta con successo, cioè che l'utente sia connesso.

Installazione e requisiti

Negli esempi useremo un oggetto della classe Nette\Security\User, che rappresenta l'utente corrente e che ottenete facendovelo passare con la dependency injection. Nei presenter basta chiamare $user = $this->getUser().

Per siti molto semplici con un'amministrazione in cui i permessi degli utenti non sono differenziati, come criterio di autorizzazione si può usare il già noto metodo isLoggedIn(). In altre parole: appena un utente è connesso, ha tutti i permessi, e viceversa.

if ($user->isLoggedIn()) { // l'utente è connesso?
	deleteItem(); // allora ha il permesso per l'operazione
}

Ruoli

Lo scopo dei ruoli è offrire un controllo più preciso sui permessi e restare indipendenti dal nome utente. Appena un utente accede, gli vengono assegnati uno o più ruoli con cui agirà. I ruoli possono essere semplici stringhe, per esempio admin, member, guest ecc. Si indicano come secondo argomento del costruttore di SimpleIdentity, come stringa oppure come array di stringhe, cioè di ruoli.

Come criterio di autorizzazione useremo ora il metodo isInRole(), che rivela se l'utente agisce nel ruolo indicato:

if ($user->isInRole('admin')) { // l'utente è nel ruolo admin?
	deleteItem(); // allora ha il permesso per l'operazione
}

Come già sapete, la disconnessione dell'utente non deve per forza cancellarne l'identità. Il metodo getIdentity() restituisce quindi ancora l'oggetto SimpleIdentity, compresi tutti i ruoli concessi. Il Nette Framework sposa il principio “meno codice, più sicurezza”, per cui scrivere meno porta a codice più sicuro. Perciò, quando controllate i ruoli, non dovete verificare se l'utente è connesso. Il metodo isInRole() lavora con i ruoli effettivi: se l'utente è connesso, si basa sui ruoli indicati nell'identità; se non è connesso, ha automaticamente il ruolo speciale guest (oppure i ruoli dell'identità ospite, se ne è stata fornita una).

Autorizzatore

Oltre ai ruoli introduciamo i termini risorsa e operazione:

  • ruolo è una proprietà dell'utente, per esempio moderatore, redattore, visitatore, utente registrato, amministratore…
  • risorsa è un'unità logica del sito: articolo, pagina, utente, voce di menu, sondaggio, presenter, …
  • operazione è un'attività concreta che l'utente può o non può compiere sulla risorsa, per esempio visualizzare, modificare, cancellare, votare, …

Un autorizzatore è un oggetto che decide se un dato ruolo ha il permesso di eseguire una certa operazione su una determinata risorsa. È un oggetto che implementa l'interfaccia Nette\Security\Authorizator con un unico 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;
	}
}

L'autorizzatore lo aggiungiamo alla configurazione come servizio del container DI:

services:
	- MyAuthorizator

Ed ecco un esempio d'uso. Attenzione: questa volta chiamiamo il metodo Nette\Security\User::isAllowed(), non quello dell'autorizzatore, quindi manca il primo parametro $role. Questo metodo chiama in sequenza MyAuthorizator::isAllowed() per tutti i ruoli dell'utente e restituisce true se almeno uno di essi ha il permesso.

if ($user->isAllowed('file')) { // l'utente può fare qualcosa con la risorsa 'file'?
	useFile();
}

if ($user->isAllowed('file', 'delete')) { // l'utente può eseguire 'delete' sulla risorsa 'file'?
	deleteFile();
}

Entrambi gli argomenti sono facoltativi; il valore predefinito null significa qualsiasi cosa.

Permission ACL

Nette porta con sé un'implementazione integrata dell'autorizzatore, la classe Nette\Security\Permission, che offre al programmatore un livello ACL (Access Control List) leggero e flessibile per gestire permessi e accessi. Lavorarci consiste nel definire ruoli, risorse e i singoli permessi. Ruoli e risorse permettono di creare gerarchie. Per spiegarlo mostriamo l'esempio di un'applicazione web:

  • guest: visitatore non registrato che può leggere e sfogliare la parte pubblica del sito, cioè leggere articoli, commenti e votare nei sondaggi.
  • registered: utente registrato e connesso, che può anche scrivere commenti.
  • admin: può gestire articoli, commenti e sondaggi.

Abbiamo definito alcuni ruoli (guest, registered e admin) e citato delle risorse (article, comment, poll), a cui gli utenti con un certo ruolo possono accedere o su cui possono compiere certe operazioni (view, vote, add, edit).

Creiamo un'istanza della classe Permission e definiamo i ruoli. Si può usare la cosiddetta ereditarietà dei ruoli, che garantisce che per esempio un utente con il ruolo admin possa fare anche ciò che può fare un normale visitatore del sito (e naturalmente di più).

$acl = new Nette\Security\Permission;

$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' eredita da 'guest'
$acl->addRole('admin', 'registered'); // 'admin' eredita da 'registered'

Ora definiamo l'elenco delle risorse a cui gli utenti possono accedere.

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

Anche le risorse possono usare l'ereditarietà; si potrebbe per esempio scrivere $acl->addResource('perex', 'article').

E ora la parte più importante. Definiamo tra loro le regole che stabiliscono chi può fare che cosa e con che cosa:

// all'inizio nessuno può fare nulla

// lasciamo che guest visualizzi articoli, commenti e sondaggi
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// e che voti nei sondaggi
$acl->allow('guest', 'poll', 'vote');

// registered eredita i permessi da guest, diamogli in più il diritto di commentare
$acl->allow('registered', 'comment', 'add');

// l'amministratore può visualizzare e modificare qualsiasi cosa
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);

E se volessimo impedire a qualcuno di accedere a una certa risorsa?

// l'amministratore non può modificare i sondaggi, sarebbe antidemocratico
$acl->deny('admin', 'poll', 'edit');

Ora che abbiamo creato l'insieme delle regole, possiamo semplicemente porre delle domande di autorizzazione:

// guest può visualizzare gli articoli?
$acl->isAllowed('guest', 'article', 'view'); // true

// guest può modificare gli articoli?
$acl->isAllowed('guest', 'article', 'edit'); // false

// guest può votare nei sondaggi?
$acl->isAllowed('guest', 'poll', 'vote'); // true

// guest può commentare?
$acl->isAllowed('guest', 'comment', 'add'); // false

Lo stesso vale per un utente registrato, che però può anche commentare:

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

L'amministratore può modificare tutto, tranne i sondaggi:

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

I permessi si possono valutare anche in modo dinamico e possiamo lasciare la decisione a un nostro callback, al quale vengono passati tutti i parametri:

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

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

Ma come affrontare la situazione in cui i soli nomi di ruoli e risorse non bastano e vorremmo definire che, per esempio, il ruolo registered può modificare la risorsa article solo se ne è l'autore? Useremo oggetti invece di stringhe: il ruolo sarà un oggetto Nette\Security\Role e la risorsa un oggetto Nette\Security\Resource. I loro metodi getRoleId() e rispettivamente getResourceId() restituiranno le stringhe originali:

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

E ora creiamo la regola:

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

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

E la domanda all'ACL si pone passando gli oggetti:

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

Un ruolo può ereditare da un ruolo o da più ruoli. Ma che succede se un antenato ha l'azione negata e l'altro consentita? Quali saranno i diritti del discendente? Lo determina il peso del ruolo: l'ultimo ruolo indicato nell'elenco degli antenati ha il peso maggiore, il primo quello minore. Dall'esempio si vede meglio:

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

$acl->addResource('backend');

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

// caso A: il ruolo admin ha peso minore del ruolo guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false

// caso B: il ruolo admin ha peso maggiore del ruolo guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true

Ruoli e risorse si possono anche rimuovere (removeRole(), removeResource()) e anche le regole si possono revocare (removeAllow(), removeDeny()). L'array di tutti i ruoli genitori diretti lo restituisce getRoleParents(). Se due entità ereditano l'una dall'altra lo dicono roleInheritsFrom() e resourceInheritsFrom(). L'esistenza di un ruolo o di una risorsa si verifica con hasRole() e hasResource(), gli elenchi di tutti i ruoli e le risorse registrati li restituiscono getRoles() e getResources(), e si può cancellare tutto con removeAllRoles() e removeAllResources().

Aggiunta come servizio

L'ACL che abbiamo creato dobbiamo passarlo alla configurazione come servizio, perché l'oggetto $user cominci a usarlo, cioè perché nel codice si possa usare $user->isAllowed('article', 'view'). A questo scopo gli scriveremo una factory:

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

E la aggiungiamo alla configurazione:

services:
	- App\Model\AuthorizatorFactory::create

Nei presenter potete poi verificare i permessi per esempio nel metodo startup():

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