Vérification des autorisations (Autorisation)
L'autorisation détermine si un utilisateur dispose des autorisations suffisantes, par exemple pour accéder à une ressource spécifique ou pour effectuer une action donnée. L'autorisation présuppose une authentification préalable réussie, c'est-à-dire que l'utilisateur est connecté.
Dans les exemples, nous utiliserons l'objet de la classe Nette\Security\User, qui représente l'utilisateur
actuel et auquel vous pouvez accéder en le faisant passer via l'injection de dépendances. Dans les presenters, il
suffit d'appeler $user = $this->getUser()
.
Pour les sites web très simples avec une administration, où les autorisations des utilisateurs ne sont pas différenciées,
il est possible d'utiliser la méthode déjà connue isLoggedIn()
comme critère d'autorisation. En d'autres termes :
dès que l'utilisateur est connecté, il dispose de toutes les autorisations et inversement.
if ($user->isLoggedIn()) { // l'utilisateur est-il connecté ?
deleteItem(); // alors il a l'autorisation pour l'opération
}
Rôles
Le but des rôles est d'offrir un contrôle plus précis des autorisations et de rester indépendant du nom d'utilisateur. À
chaque utilisateur, dès sa connexion, nous attribuons un ou plusieurs rôles dans lesquels il agira. Les rôles peuvent être de
simples chaînes de caractères, par exemple admin
, member
, guest
, etc. Ils sont indiqués
comme deuxième paramètre du constructeur SimpleIdentity
, soit comme une chaîne, soit comme un tableau de
chaînes – rôles.
Comme critère d'autorisation, nous utiliserons maintenant la méthode isInRole()
, qui indique si l'utilisateur
agit dans le rôle donné :
if ($user->isInRole('admin')) { // l'utilisateur est-il dans le rôle admin ?
deleteItem(); // alors il a l'autorisation pour l'opération
}
Comme vous le savez déjà, après la déconnexion de l'utilisateur, son identité ne doit pas nécessairement être
supprimée. Ainsi, la méthode getIdentity()
continue de renvoyer l'objet SimpleIdentity
, y compris tous
les rôles attribués. Le Nette Framework adhère au principe « moins de code, plus de sécurité », où moins d'écriture
conduit à un code plus sécurisé, donc lors de la vérification des rôles, vous n'avez pas besoin de vérifier en plus si
l'utilisateur est connecté. La méthode isInRole()
travaille avec les rôles effectifs, c'est-à-dire que si
l'utilisateur est connecté, elle se base sur les rôles indiqués dans l'identité, s'il n'est pas connecté, il a
automatiquement le rôle spécial guest
.
Autorisateur
En plus des rôles, nous introduirons également les concepts de ressource et d'opération :
- rôle est une propriété de l'utilisateur – par exemple, modérateur, rédacteur, visiteur, utilisateur enregistré, administrateur…
- ressource (resource) est un élément logique du site web – article, page, utilisateur, élément de menu, sondage, presenter, …
- opération (operation) est une activité spécifique que l'utilisateur peut ou ne peut pas effectuer avec la ressource – par exemple, supprimer, modifier, créer, voter, …
L'autorisateur est un objet qui décide si un rôle donné a la permission d'effectuer une opération spécifique
sur une ressource donnée. Il s'agit d'un objet implémentant l'interface Nette\Security\Authorizator avec une seule
méthode 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;
}
}
Nous ajoutons l'autorisateur à la configuration en tant que service du conteneur DI :
services:
- MyAuthorizator
Et voici un exemple d'utilisation. Attention, cette fois nous appelons la méthode
Nette\Security\User::isAllowed()
, et non l'autorisateur, donc le premier paramètre $role
n'est pas là.
Cette méthode appelle MyAuthorizator::isAllowed()
successivement pour tous les rôles de l'utilisateur et renvoie
true si au moins l'un d'eux a la permission.
if ($user->isAllowed('file')) { // l'utilisateur peut-il faire n'importe quoi avec la ressource 'file' ?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // peut-il effectuer 'delete' sur la ressource 'file' ?
deleteFile();
}
Les deux paramètres sont facultatifs, la valeur par défaut null
signifie n'importe quoi.
Permission ACL
Nette est livré avec une implémentation intégrée de l'autorisateur, la classe Nette\Security\Permission qui fournit au programmeur une couche ACL (Access Control List) légère et flexible pour gérer les autorisations et les accès. Son utilisation consiste à définir des rôles, des ressources et des autorisations individuelles. Les rôles et les ressources permettent de créer des hiérarchies. Pour expliquer, prenons l'exemple d'une application web :
guest
: visiteur non connecté, qui peut lire et parcourir la partie publique du site, c'est-à-dire lire les articles, les commentaires et voter aux sondagesregistered
: utilisateur enregistré connecté, qui peut en plus commenteradmin
: peut gérer les articles, les commentaires et les sondages
Nous avons donc défini certains rôles (guest
, registered
et admin
) et mentionné des
ressources (article
, comment
, poll
), auxquelles les utilisateurs avec un certain rôle
peuvent accéder ou effectuer certaines opérations (view
, vote
, add
,
edit
).
Nous créons une instance de la classe Permission et définissons les rôles. Il est possible d'utiliser l'héritage des
rôles, qui garantit que, par exemple, un utilisateur avec le rôle d'administrateur (admin
) peut faire ce qu'un
visiteur ordinaire du site peut faire (et bien sûr plus).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // 'registered' hérite de 'guest'
$acl->addRole('admin', 'registered'); // et 'admin' hérite de lui
Maintenant, définissons également la liste des ressources auxquelles les utilisateurs peuvent accéder.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
Les ressources peuvent également utiliser l'héritage, il serait possible par exemple de spécifier
$acl->addResource('perex', 'article')
.
Et maintenant, le plus important. Définissons entre eux les règles déterminant qui peut faire quoi avec quoi :
// d'abord, personne ne peut rien faire
// que guest puisse consulter les articles, les commentaires et les sondages
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// et dans les sondages, en plus, voter
$acl->allow('guest', 'poll', 'vote');
// l'utilisateur enregistré hérite des droits de guest, donnons-lui en plus le droit de commenter
$acl->allow('registered', 'comment', 'add');
// l'administrateur peut consulter et modifier n'importe quoi
$acl->allow('admin', $acl::All, ['view', 'edit', 'add']);
Et si nous voulons empêcher quelqu'un d'accéder à une ressource spécifique ?
// l'administrateur ne peut pas modifier les sondages, ce serait antidémocratique
$acl->deny('admin', 'poll', 'edit');
Maintenant que nous avons créé la liste des règles, nous pouvons simplement poser des questions d'autorisation :
// guest peut-il consulter les articles ?
$acl->isAllowed('guest', 'article', 'view'); // true
// guest peut-il modifier les articles ?
$acl->isAllowed('guest', 'article', 'edit'); // false
// guest peut-il voter aux sondages ?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// guest peut-il commenter ?
$acl->isAllowed('guest', 'comment', 'add'); // false
La même chose s'applique à l'utilisateur enregistré, mais il peut aussi commenter :
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
L'administrateur peut tout modifier, sauf les sondages :
$acl->isAllowed('admin', 'poll', 'vote'); // true
$acl->isAllowed('admin', 'poll', 'edit'); // false
$acl->isAllowed('admin', 'comment', 'edit'); // true
Les autorisations peuvent également être évaluées dynamiquement et nous pouvons laisser la décision à notre propre rappel, auquel tous les paramètres sont transmis :
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return /* ... */;
};
$acl->allow('registered', 'comment', null, $assertion);
Mais comment gérer, par exemple, une situation où les noms des rôles et des ressources ne suffisent pas, mais où nous
voudrions définir que, par exemple, le rôle registered
ne peut modifier la ressource article
que s'il
en est l'auteur ? Au lieu de chaînes de caractères, nous utiliserons des objets, le rôle sera un objet Nette\Security\Role et la ressource un objet Nette\Security\Resource. Leurs méthodes
getRoleId()
resp. getResourceId()
renverront les chaînes originales :
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';
}
}
Et maintenant, créons la règle :
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // objet Registered
$resource = $acl->getQueriedResource(); // objet Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
Et la requête à l'ACL se fait en passant les objets :
$user = new Registered(/* ... */);
$article = new Article(/* ... */);
$acl->isAllowed($user, $article, 'edit');
Un rôle peut hériter d'un autre rôle ou de plusieurs rôles. Mais que se passe-t-il si un ancêtre a une action interdite et un autre autorisée ? Quels seront les droits du descendant ? Cela est déterminé par le poids du rôle – le dernier rôle mentionné dans la liste des ancêtres a le poids le plus élevé, le premier rôle mentionné a le poids le plus faible. C'est plus clair avec un exemple :
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// cas A : le rôle admin a moins de poids que le rôle guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// cas B : le rôle admin a plus de poids que guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Les rôles et les ressources peuvent également être supprimés (removeRole()
, removeResource()
),
les règles peuvent également être annulées (removeAllow()
, removeDeny()
). Le tableau de tous les
rôles parents directs est renvoyé par getRoleParents()
, si deux entités héritent l'une de l'autre est renvoyé
par roleInheritsFrom()
et resourceInheritsFrom()
.
Ajout en tant que services
Nous devons transmettre notre ACL créée à la configuration en tant que service, afin que l'objet $user
commence
à l'utiliser, c'est-à-dire pour pouvoir utiliser dans le code par exemple $user->isAllowed('article', 'view')
.
À cette fin, nous écrirons une factory pour cela :
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;
}
}
Et nous l'ajoutons à la configuration :
services:
- App\Model\AuthorizatorFactory::create
Dans les presenters, vous pouvez ensuite vérifier les autorisations, par exemple dans la méthode
startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Interdit', 403);
}
}