Verifica dei permessi (Autorizzazione)
L'autorizzazione verifica se un utente ha permessi sufficienti, ad esempio, per accedere a una determinata risorsa o per eseguire una determinata azione. L'autorizzazione presuppone una precedente autenticazione riuscita, ovvero che l'utente sia loggato.
Negli esempi useremo l'oggetto della classe Nette\Security\User, che rappresenta l'utente corrente e
al quale potete accedere facendovelo passare tramite dependency injection. Nei presenter basta solo
chiamare $user = $this->getUser()
.
Per siti web molto semplici con amministrazione, dove non si distinguono i permessi degli utenti, è possibile utilizzare come
criterio di autorizzazione il già noto metodo isLoggedIn()
. In altre parole: non appena l'utente è loggato, ha
tutti i permessi e viceversa.
if ($user->isLoggedIn()) { // l'utente è loggato?
deleteItem(); // allora ha il permesso per l'operazione
}
Ruoli
Lo scopo dei ruoli è offrire un controllo più preciso dei permessi e rimanere indipendenti dal nome utente. A ogni utente,
subito al momento del login, assegniamo uno o più ruoli in cui opererà. I ruoli possono essere semplici stringhe, ad esempio
admin
, member
, guest
, ecc. Vengono indicati come secondo parametro del costruttore
SimpleIdentity
, sia come stringa che come array di stringhe – ruoli.
Come criterio di autorizzazione ora useremo il metodo isInRole()
, che rivela se l'utente opera nel ruolo
specificato:
if ($user->isInRole('admin')) { // l'utente è nel ruolo di admin?
deleteItem(); // allora ha il permesso per l'operazione
}
Come già sapete, dopo il logout dell'utente la sua identità non deve necessariamente essere cancellata. Quindi il metodo
getIdentity()
continua a restituire l'oggetto SimpleIdentity
, inclusi tutti i ruoli assegnati. Nette
Framework segue il principio “less code, more security”, dove meno scrittura porta a un codice più sicuro, quindi nel
verificare i ruoli non è necessario verificare anche se l'utente è loggato. Il metodo isInRole()
lavora con
i ruoli effettivi, ovvero se l'utente è loggato, si basa sui ruoli indicati nell'identità, se non è loggato, ha
automaticamente il ruolo speciale guest
.
Autorizzatore
Oltre ai ruoli, introdurremo anche i concetti di risorsa e operazione:
- ruolo è una proprietà dell'utente – ad es. moderatore, redattore, visitatore, utente registrato, amministratore…
- risorsa (resource) è un elemento logico del sito web – articolo, pagina, utente, voce di menu, sondaggio, presenter, …
- operazione (operation) è un'attività specifica che l'utente può o non può fare con la risorsa – ad esempio cancellare, modificare, creare, votare, …
L'autorizzatore è un oggetto che decide se un dato ruolo ha il permesso di eseguire una determinata operazione
su una determinata risorsa. Si tratta di 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;
}
}
Aggiungiamo l'autorizzatore alla configurazione come servizio del container DI:
services:
- MyAuthorizator
E segue un esempio di utilizzo. Attenzione, questa volta chiamiamo il metodo Nette\Security\User::isAllowed()
, non
l'autorizzatore, quindi non c'è il primo parametro $role
. Questo metodo chiama
MyAuthorizator::isAllowed()
progressivamente 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 qualsiasi cosa con la risorsa 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // può eseguire 'delete' sulla risorsa 'file'?
deleteFile();
}
Entrambi i parametri sono facoltativi, il valore predefinito null
significa qualsiasi cosa.
Permission ACL
Nette viene fornito con un'implementazione integrata dell'autorizzatore, ovvero la classe Nette\Security\Permission che fornisce al programmatore uno strato ACL (Access Control List) leggero e flessibile per la gestione dei permessi e degli accessi. Il lavoro con essa consiste nella definizione di ruoli, risorse e singoli permessi. Ruoli e risorse consentono di creare gerarchie. Per spiegare, mostreremo un esempio di applicazione web:
guest
: visitatore non loggato, che può leggere e navigare nella parte pubblica del sito, ovvero leggere articoli, commenti e votare nei sondaggiregistered
: utente registrato loggato, che inoltre può commentareadmin
: può gestire articoli, commenti e sondaggi
Abbiamo quindi definito alcuni ruoli (guest
, registered
e admin
) e menzionato risorse
(article
, comment
, poll
), alle quali gli utenti con un determinato ruolo possono accedere
o eseguire determinate operazioni (view
, vote
, add
, edit
).
Creiamo un'istanza della classe Permission e definiamo i ruoli. È possibile utilizzare la cosiddetta ereditarietà dei
ruoli, che garantisce che, ad esempio, un utente con il ruolo di amministratore (admin
) possa fare anche ciò che fa
un normale visitatore del sito (e ovviamente anche di più).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' eredita da 'guest'
$acl->addRole('admin', 'registered'); // e da esso eredita 'admin'
Ora definiamo anche 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à, sarebbe possibile ad esempio specificare
$acl->addResource('perex', 'article')
.
E ora la cosa più importante. Definiamo tra loro le regole che determinano chi può fare cosa con cosa:
// inizialmente nessuno può fare nulla
// permettiamo a guest di visualizzare articoli, commenti e sondaggi
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// e nei sondaggi inoltre anche votare
$acl->allow('guest', 'poll', 'vote');
// registrato eredita i diritti da guest, gli diamo inoltre 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']);
Cosa succede se vogliamo impedire a qualcuno l'accesso a una determinata risorsa?
// l'amministratore non può modificare i sondaggi, sarebbe antidemocratico
$acl->deny('admin', 'poll', 'edit');
Ora, quando abbiamo creato l'elenco delle regole, possiamo semplicemente porre 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 l'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 possono anche essere valutati dinamicamente 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 risolvere, ad esempio, la situazione in cui non bastano solo i nomi dei ruoli e delle risorse, ma vorremmo definire
che, ad esempio, il ruolo registered
può modificare la risorsa article
solo se ne è l'autore? Invece
delle stringhe useremo oggetti, il ruolo sarà un oggetto Nette\Security\Role e la risorsa Nette\Security\Resource. I loro metodi
getRoleId()
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 query sull'ACL viene eseguita passando gli oggetti:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Un ruolo può ereditare da un altro ruolo o da più ruoli. Ma cosa succede se un antenato ha l'azione vietata e l'altro permessa? Quali saranno i diritti del discendente? Si determina in base al peso del ruolo – l'ultimo ruolo indicato nell'elenco degli antenati ha il peso maggiore, il primo ruolo indicato quello minore. È più chiaro con un esempio:
$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 meno peso del ruolo guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// caso B: il ruolo admin ha più peso di guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Ruoli e risorse possono anche essere rimossi (removeRole()
, removeResource()
), è possibile annullare
anche le regole (removeAllow()
, removeDeny()
). L'array di tutti i ruoli genitore diretti viene
restituito da getRoleParents()
, se due entità ereditano l'una dall'altra viene restituito da
roleInheritsFrom()
e resourceInheritsFrom()
.
Aggiunta come servizi
L'ACL che abbiamo creato deve essere passato alla configurazione come servizio affinché l'oggetto $user
inizi ad
usarlo, ovvero affinché sia possibile usare nel codice ad es. $user->isAllowed('article', 'view')
. A tal fine,
scriveremo una factory per esso:
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 quindi verificare i permessi ad esempio nel metodo startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}