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é.

Installation et prérequis

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 sondages
  • registered : utilisateur enregistré connecté, qui peut en plus commenter
  • admin : 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);
	}
}
version: 4.0