Controllo degli accessi (autorizzazione)
L'autorizzazione determina se un utente ha privilegi sufficienti, ad esempio, per accedere a una risorsa specifica o per eseguire un'azione. L'autorizzazione presuppone che l'utente sia stato precedentemente autenticato, cioè che abbia effettuato il login.
Negli esempi, utilizzeremo un oggetto di classe Nette\Security\User, che rappresenta l'utente corrente e
che si ottiene passandoglielo tramite dependency
injection. Nei presenter è sufficiente chiamare $user = $this->getUser()
.
Per siti web molto semplici con amministrazione, in cui i diritti degli utenti non sono distinti, è possibile utilizzare il
metodo già noto come criterio di autorizzazione isLoggedIn()
. In altre parole: una volta che un utente è loggato,
ha i permessi per tutte le azioni e viceversa.
if ($user->isLoggedIn()) { // l'utente è connesso?
deleteItem(); // in caso affermativo, può cancellare un elemento.
}
Ruoli
Lo scopo dei ruoli è quello di offrire una gestione più precisa dei permessi e di rimanere indipendenti dal nome dell'utente.
Non appena l'utente si collega, gli vengono assegnati uno o più ruoli. I ruoli stessi possono essere semplici stringhe, ad
esempio admin
, member
, guest
, ecc. Sono specificati nel secondo parametro del costruttore
SimpleIdentity
, come stringa o come array.
Come criterio di autorizzazione, si utilizzerà il metodo isInRole()
, che verifica se l'utente è nel ruolo
indicato:
if ($user->isInRole('admin')) { // L'utente ha il ruolo di amministratore?
deleteItem(); // in caso affermativo, l'utente può cancellare un elemento.
}
Come si sa, la disconnessione dell'utente non cancella la sua identità. Pertanto, il metodo getIdentity()
restituisce ancora l'oggetto SimpleIdentity
, comprensivo di tutti i ruoli concessi. Il framework Nette aderisce al
principio “meno codice, più sicurezza”, quindi quando si controllano i ruoli, non è necessario verificare se l'utente è
connesso. Il metodo isInRole()
funziona con ruoli effettivi, cioè se l'utente è connesso, vengono utilizzati
i ruoli assegnati all'identità, se non è connesso, viene invece utilizzato un ruolo speciale automatico guest
.
Autorizzatore
Oltre ai ruoli, introdurremo i termini risorsa e operazione:
- ruolo è un attributo dell'utente – ad esempio moderatore, editore, visitatore, utente registrato, amministratore, …
- risorsa è un'unità logica dell'applicazione (articolo, pagina, utente, voce di menu, sondaggio, presentatore, …).
- operazione è un'attività specifica che l'utente può o non può fare con la risorsa – visualizzare, modificare, cancellare, votare, …
Un autorizzatore è un oggetto che decide se un determinato ruolo ha il permesso di eseguire una certa operazione
con una specifica risorsa. È un oggetto che implementa l'interfaccia Nette\Security\Authorizator con un solo 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 contenitore DI:
services:
- MyAuthorizator
E quello che segue è un esempio di utilizzo. Si noti che questa volta chiamiamo il metodo
Nette\Security\User::isAllowed()
, non quello dell'autenticatore, quindi non c'è il primo parametro
$role
. Questo metodo chiama MyAuthorizator::isAllowed()
in sequenza per tutti i ruoli utente e
restituisce true se almeno uno di essi ha l'autorizzazione.
if ($user->isAllowed('file')) { // L'utente può fare tutto con la risorsa 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // l'utente può cancellare una risorsa 'file'?
deleteFile();
}
Entrambi gli argomenti sono opzionali e il loro valore predefinito significa tutto.
Permessi ACL
Nette dispone di un'implementazione integrata dell'autorizzatore, la classe Nette\Security\Permission, che offre un livello ACL (Access Control List) leggero e flessibile per il controllo dei permessi e degli accessi. Quando si lavora con questa classe, si definiscono ruoli, risorse e permessi individuali. I ruoli e le risorse possono formare gerarchie. Per spiegarlo, mostreremo un esempio di applicazione web:
guest
: visitatore non loggato, autorizzato a leggere e navigare nella parte pubblica del web, cioè a leggere articoli, commentare e votare nei sondaggiregistered
: utente loggato, che può inoltre inserire commentiadmin
: può gestire articoli, commenti e sondaggi
Abbiamo quindi definito alcuni ruoli (guest
, registered
e admin
) e menzionato le risorse
(article
, comments
, poll
), alle quali gli utenti possono accedere o compiere azioni
(view
, vote
, add
, edit
).
Creiamo un'istanza della classe Permission e definiamo i ruoli. È possibile utilizzare l'ereditarietà dei ruoli, che
garantisce che, ad esempio, un utente con il ruolo admin
possa fare ciò che può fare un normale visitatore del sito
web (e naturalmente anche di più).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' eredita da 'guest'
$acl->addRole('admin', 'registered'); // e 'admin' eredita da 'registered'
Ora definiremo un elenco di risorse a cui gli utenti possono accedere:
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Le risorse possono anche utilizzare l'ereditarietà, ad esempio possiamo aggiungere
$acl->addResource('perex', 'article')
.
E ora la cosa più importante. Definiremo tra loro delle regole che determinano chi può fare cosa:
// tutto è negato ora
// permettiamo all'ospite di visualizzare articoli, commenti e sondaggi
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// e anche votare nei sondaggi
$acl->allow('guest', 'poll', 'vote');
// il registrato eredita i permessi da guesta, gli permetteremo anche di commentare
$acl->allow('registered', 'comment', 'add');
// L'amministratore può vedere e modificare qualsiasi cosa
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
E se volessimo impedire a qualcuno di accedere a una risorsa?
// L'amministratore non può modificare i sondaggi, sarebbe antidemocratico.
$acl->deny('admin', 'poll', 'edit');
Ora, una volta creato l'insieme di regole, possiamo semplicemente chiedere l'autorizzazione:
// L'ospite può visualizzare gli articoli?
$acl->isAllowed('guest', 'article', 'view'); // true
// L'ospite può modificare un articolo?
$acl->isAllowed('guest', 'article', 'edit'); // false
// L'ospite può votare nei sondaggi?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// l'ospite può aggiungere commenti?
$acl->isAllowed('guest', 'comment', 'add'); // false
Lo stesso vale per un utente registrato, ma 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 al 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 una situazione in cui i nomi dei ruoli e delle risorse non sono sufficienti, cioè vorremmo definire che,
per esempio, un ruolo registered
può modificare una risorsa article
solo se ne è l'autore? Useremo
oggetti invece di stringhe, il ruolo sarà l'oggetto Nette\Security\Role e la risorsa Nette\Security\Resource. I metodi
getRoleId()
e 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 una regola:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // oggetto Registrato
$resource = $acl->getQueriedResource(); // oggetto Articolo
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
L'ACL viene interrogata passando degli oggetti:
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Un ruolo può ereditare da uno o più ruoli. Ma cosa succede se a un antenato è consentita una certa azione e all'altro è negata? Allora entra in gioco il peso del ruolo: l'ultimo ruolo dell'array di ruoli da ereditare ha il peso maggiore, il primo il minore:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// esempio A: il ruolo admin ha un peso inferiore al ruolo guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// esempio B: il ruolo admin ha un peso maggiore del ruolo guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
I ruoli e le risorse possono anche essere rimossi (removeRole()
, removeResource()
), le regole
possono anche essere annullate (removeAllow()
, removeDeny()
). L'array di tutti i ruoli genitori diretti
restituisce getRoleParents()
. Se due entità ereditano l'una dall'altra restituisce roleInheritsFrom()
e
resourceInheritsFrom()
.
Aggiungere come servizio
Dobbiamo aggiungere la ACL da noi creata alla configurazione come servizio, in modo che possa essere utilizzata dall'oggetto
$user
, cioè in modo da poterla usare nel codice, ad esempio $user->isAllowed('article', 'view')
.
A questo scopo, scriveremo un factory per questo servizio:
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 lo aggiungeremo alla configurazione:
services:
- App\Model\AuthorizatorFactory::create
Nei presentatori, è possibile verificare le autorizzazioni nel metodo startup()
, ad esempio:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}