28 Juil, 2020

Les autorisations sous CakePHP 4

Mise en place d'un système d'autorisations sous CakePHP4.

En complément du plugin Authentication de CakePHP4, vous souhaitez probablement sécuriser votre application en définissant les règles d’autorisations à des ressources.

Installation et configuration

Exécutez la commande suivante à la racine du projet afin d’installer le plugin :

composer require cakephp/authorization:^2.0

Chargez ensuite le plugin dans la méthode bootstrap située dans le fichier src/Application.php :

$this->addPlugin('Authorization');

Le plugin s’implémente dans votre projet en tant que middleware.
Toujours dans le fichier src/Application.php, importez les classes suivantes :

use Authorization\AuthorizationService;
use Authorization\AuthorizationServiceInterface;
use Authorization\AuthorizationServiceProviderInterface;
use Authorization\Middleware\AuthorizationMiddleware;
use Authorization\Policy\OrmResolver;
use Psr\Http\Message\ResponseInterface;
use Psr\HttpMessage\ServerRequestInterface;

Ajoutez ensuite l’interface AuthorizationProviderInterface aux interfaces implémentées sur votre application :

class Application extends BaseApplication implements AuthorizationServiceProviderInterface

Puis ajoutez le middleware AuthorizationMiddleware à la file d’attente des middlewares de votre application :

        $middlewareQueue
            ...
            ->add(new AuthorizationMiddleware($this));

Ce middleware appelle ainsi la fonction intitulée getAuthorizationService à chaque requête. Ajoutez cette méthode à la suite de votre fichier Application.php :

public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
{
    $resolver = new OrmResolver();

    return new AuthorizationService($resolver);
}

Nous avons ici ajouté l’OrmResolver. Ce dernier nous permet de dicter les règles d’autorisations pour accéder à des ressources.

Chargez ensuite le composant Authorization dans votre AppController :

$this->loadComponent('Authorization.Authorization');

OrmResolver

Nous allons désormais voir comment définir les règles d’autorisations d’accès et de manipulations à nos données.
Prenons un exemple simple, vous avez une table articles. Chaque article a un seul auteur et vous souhaitez que seul l’auteur de l’article puisse l’éditer, la méthode d’édition s’intitulant edit.
Pour cela créez un fichier ArticlePolicy.php dans le dossier src/Policy et copiez-y le code suivant :

<?php

namespace AppPolicy;

use AppModelEntityArticle;
use AuthorizationIdentityInterface;

class ArticlePolicy
{
    public function canEdit(IdentityInterface $user, Article $article)
    {
        return $article->author_id === $user->id;
    }
}

Les fichiers Policy permettent de déclarer les règles d’autorisations d’accès à des ressources. Chaque méthode est préfixée du mot « can ». Ensuite vous pouvez utiliser le nom de votre méthode dans votre controller ou bien utiliser un autre nom.
Ici canEdit vérifie que l’utilisateur connecté est bien l’auteur de l’article en comparant l’id de ces derniers. Si cette méthode rend true alors l’utilisateur peut accéder à la méthode edit sinon une exception est levée et une page d’erreur s’affiche.

Vous pouvez ainsi insérer la ligne de code suivante dans votre méthode edit qui fera le lien avec canEdit automatiquement car les conventions de nommage ont été respectées.

public function edit($id)
{
    $article = $this->Articles->get($id);
    $this->Authorization->authorize($article);
}

Vous pouvez précisez en deuxième paramètre le nom de la méthode à appliquer. Par exemple si nous créons canUpdate plutôt que canEdit, nous écrivons alors : 

public function edit($id)
{
    $article = $this->Articles->get($id);
    $this->Authorization->authorize($article, 'update');
}

Vous pouvez également définir des règles au niveau des requêtes. Par exemple vous souhaitez vous assurer que sur une page donnée, chaque auteur ne peut voir que les articles qu’il a créés.
Pour cela créez un fichier ArticlesTablePolicy.php dans le dossier src/Policy et insérez le code suivant :

<?php

namespace App\Policy;
use Cake\ORM\Query;
use Authorization\IdentityInterface;
class ArticlesTablePolicy
{
    public function scopeAuthor(IdentityInterface $user, Query $query)
    {
        return $query->where(['Articles.author_id' => $user->id]);
    }
}

Les conventions de nommage sont les mêmes que précédemment, la différence ici étant que l’on préfixe nos méthode par « scope ».
Ensuite vous pouvez utiliser cette règle dans une méthode de votre controller comme ici :

public function index()
{
    $articles = $this->Articles->find();
    $this->Authorization->applyScope($articles, 'author');
}

MapResolver

En complément de l’OrmResolver vous pouvez utiliser le MapResolver. Ici nous verrons comment définir les règles d’accès à certaines méthodes selon le rôle de l’utilisateur.

Dans le fichier Application.php, importez les classes suivantes :

use App\Policy\RequestPolicy;
use Authorization\Policy\MapResolver;
use Cake\Http\ServerRequest;
use Authorization\Middleware\RequestAuthorizationMiddleware;
use Authorization\Policy\ResolverCollection;

Ajoutez ensuite RequestAuthorizationMiddleware à la liste des middlewares :

        $middlewareQueue
            ...
            ->add(new RequestAuthorizationMiddleware());

Dans la fonction getAuthorizationService du fichier Application.php, remplacez le contenu du code par celui-ci :

    /**
     * Returns a service provider instance.
     *
     * @param ServerRequestInterface $request
     * @return AuthorizationServiceInterface
     */
    public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
    {
        $ormResolver = new OrmResolver();
        $mapResolver = new MapResolver();
        $mapResolver->map(ServerRequest::class, RequestPolicy::class);

        $resolver = new ResolverCollection([$mapResolver, $ormResolver]);

        return new AuthorizationService($resolver);
    }

Créez ensuite le fichier RequestPolicy.php dans le dossier src/Policy.
Voici un exemple simple ou nous autorisons l’accès selon le rôle de l’utilisateur et le préfixe dans lequel il se situe. Ici il est nécessaire d’avoir le rôle admin pour accéder à toutes actions se trouvant dans le préfixe Admin.

<?php

namespace App\Policy;
use Authorization\Policy\RequestPolicyInterface;
use Cake\Http\ServerRequest;
class RequestPolicy implements RequestPolicyInterface
{
    /**
     * Method to check if the request can be accessed
     *
     * @param AuthorizationIdentityInterface|null Identity
     * @param CakeHttpServerRequest $request Server Request
     * @return bool
     */
    public function canAccess($identity, ServerRequest $request)
    {
        if (!$request->getParam('prefix')) {            
            return true;
        }

        if ($request->getParam('prefix') === 'Admin') {
            return $identity->role === 'admin';
        }

        return false;
    }
}

En conclusion

Ce nouveau système de gestion des autorisations nécessitant certes plus d’étapes de mise en place et donc une installation relativement plus complexe, permet non seulement une meilleure configuration mais aussi d’avoir un projet plus propre et maintenable.

En effet la méthode isAuthorized de l’ancienne version est utilisée dans les controllers ce qui les surchargent et tasse le code dans une seule méthode. Désormais ces logiques de code se trouvent dans des fichiers adaptés où l’on peut organiser nos autorisations dans différentes méthodes qui pourront être utilisées dans les controllers.

Aussi, contrairement à l’ancienne version où l’on pouvait définir une URL de redirection accompagnée d’un message Flash en cas d’interdiction d’accès, une exception Forbidden est désormais levée. Ainsi l’utilisateur sera redirigé automatiquement vers une page d’erreur.