Makina Blog

Le blog Makina-corpus

Access Control : une biblio­thèque PHP pour gérer des droits d’ac­cès


Suite à un projet de gestion métier opéra­tion­nel dont la durée de vie et la main­te­nance sont à long termes, nous avons expé­ri­menté un passage de celui-ci sur l’archi­tec­ture hexa­go­nale et la clean archi­tec­ture.

Sommaire

Dans cet article, ce qui nous inté­resse est la struc­ture en couches appli­ca­tives aussi parfois appe­lée struc­ture en oignon, avec au centre le Domaine où est implé­menté le métier du client, indé­pen­dant de tout code externe, et plus à l’ex­té­rieur la couche Infra­struc­ture, qui porte les implé­men­ta­tions concrètes des inter­faces du domaine, en utili­sant alors des biblio­thèques tierces. Nous avons récem­ment abouti un projet de gestion métier opéra­tion­nel, dont la durée de vie et la main­te­nance sont plani­fiées pour de nombreuses années. Dans ce contexte, nous avons expé­ri­menté un passage de celui-ci sur l’archi­tec­ture hexa­go­nale et la clean archi­tec­ture.

Dans ce contexte, nous avions besoin d’une méthode pour implé­men­ter les véri­fi­ca­tions de droits d’ac­cès dans notre domaine métier sans le coupler à du code exté­rieur. La solu­tion que nous avons déve­lop­pée est une biblio­thèque de véri­fi­ca­tion des droits d’ac­cès qui propose une API en AOP pour Aspect Orien­tend Program­ming, utili­sant les Attribute de PHP 8.0.

L’idée globale est de s’as­su­rer que toute dépen­dance externe, y compris le frame­work Symfony lui-même, soit des compo­sants discrets au sein du code métier, et puisse être rempla­cée sans aucune impli­ca­tion, ni contrainte.

Une courte intro­duc­tion

Le problème des droits d’ac­cès

Le premier problème est de véri­fier les droits d’ac­cès en plusieurs points stra­té­giques que nous appe­lons des PEP ou Policy Enfor­ce­ment Point :

  • Lorsqu’une requête HTTP arrive, avant d’exé­cu­ter le contrô­leur par exemple.
  • Lorsqu’une commande s’ap­prête à être exécu­tée dans le bus de message, avant d’exé­cu­ter son hand­ler.
  • À diffé­rents autres endroits plus margi­naux, selon les choix d’ar­chi­tec­ture qui ont été faits.

Pour ce faire, nous avons plusieurs modèles de véri­fi­ca­tion des droits d’ac­cès :

  • RBAC pour Role Based Access Control, ce que tradi­tion­nel­le­ment la plupart des gens utilisent en première inten­tion sur un projet Symfony via la méthode AbstractController::isGranted(). Par exemple, lorsque la gestion des droits d’ac­cès reste simple. Nous restons ici en dehors du domaine métier.
  • ABAC pour Attri­bute Based Access Control, qui déno­mine des méthodes de véri­fi­ca­tion des droits d’ac­cès en utili­sant des valeurs présentes dans les enti­tés ciblées par cette véri­fi­ca­tion de droits d’ac­cès. Lorsque nous utili­sons cette méthode, nous péné­trons fran­che­ment dans le domaine métier.

La liste n’est pas exhaus­tive, mais ces deux exemples illus­trent un fait : les véri­fi­ca­tions de droits d’ac­cès peuvent se baser sur des règles déri­vées de l’iden­tité de la personne et donc en dehors du domaine, mais aussi sur des règles métier couplées à l’état du domaine.

Tradi­tion­nel­le­ment, pour des véri­fi­ca­tions de droit d’ac­cès, dans une appli­ca­tion Symfony, nous allons utili­ser les méthodes que nous offrent le frame­work. La plus parlante est AbstractController::isGranted() et souvent l’im­plé­men­ta­tion de Symfony\Component\Security\Core\Authorization\Voter\VoterInterface. Mais dans notre domaine métier décou­plé, le but est de nous disso­cier du frame­work : nous devons donc nous débar­ras­ser des concepts du frame­work tels que les Voter.

Un peu de voca­bu­laire

Tout d’abord, pour lire la suite de cet article, vous devriez prendre connais­sance de ce glos­saire :

  • Quand on parle d’Access Control, on parle plus géné­ra­le­ment de la véri­fi­ca­tion de droits d’ac­cès, à ne pas confondre avec les ACL pour Access Control List qui repré­sente une méthode parti­cu­lière de vali­da­tion de droits d’ac­cès.
  • Un modèle, ici, est une méthode parti­cu­lière de véri­fi­ca­tion de droits d’ac­cès, comme RBAC pour Role Based Access Control, LBAC pour Lattice Based Access Control ou encore PBAC pour Permis­sion Based Access Control. Il existe de nombreux autres modèles que ceux qui viennent d’être cités.
  • Le PDP pour Policy Deci­sion Point est un compo­sant logi­ciel qui à partir d’une ou plusieurs poli­cies et un contexte appli­ca­tif va répondre tout simple­ment oui / allow ou non / deny.
  • Le PEP pour Policy Enfor­ce­ment Point est un endroit parti­cu­lier dans le code d’un logi­ciel où on appelle le PDP. Géné­ra­le­ment dans une appli­ca­tion web on trouve des PEP sur l’ar­ri­vée d’une requête, au début de l’exé­cu­tion d’un contrô­leur, en entrée d’un bus de message, etc…
  • Nous n’en parle­rons pas dans cet article, mais il est inté­res­sant que certaines appli­ca­tions ou systèmes d’in­for­ma­tion plus complexes disposent en sus d’un PAP pour Policy Admi­nis­tra­tion Point, ainsi que le PRP pour Policy Retrie­val Point, compo­sants logi­ciels, parfois externes à l’ap­pli­ca­tion qui les utilise, qui permettent à la fois la confi­gu­ra­tion des poli­cies et leur stockage déporté en dehors de l’ap­pli­ca­tion opéra­tion­nelle.
  • Nous véri­fions toujours les droits d’ac­cès à une resource, elle peut être une entité métier, une section d’un site, une page, ou tout autre concept auquel un utili­sa­teur peut accé­der.
  • Nous véri­fions toujours les droits d’ac­cès pour un subjet, repré­sen­tant une iden­tité, un utili­sa­teur ou un acteur, qui peut être une personne physique ou appli­ca­tion tierce, qui se connecte à l’ap­pli­ca­tion proté­gée pour accé­der à ses données ou fonc­tion­na­li­tés.

Aspect Orien­ted Program­ming

Comme expliqué plus haut, nous voulons trou­ver un moyen de :

  • Suppri­mer les dépen­dances expli­cites au frame­work pour la gestion des droits d’ac­cès.
  • Malgré tout, conti­nuer à utili­ser l’ou­tillage qu’il nous offre, car il implé­mente déjà des choses dont nous avons besoin.
  • Aller au-delà du frame­work et s’as­su­rer qu’on peut rempla­cer son implé­men­ta­tion si on devait en chan­ger plus tard.
  • Pouvoir défi­nir et implé­men­ter nos propres PEP de façon simple.
  • Pour aller plus loin, conser­ver expli­ci­te­ment la défi­ni­tion de ces droits d’ac­cès dans le code du domaine, pour éviter des allers-retours inces­sants entre de la confi­gu­ra­tion du frame­work et notre domaine, et garder tout le métier… dans la couche métier.

Pour répondre à tous ces points, nous avons décidé d’im­plé­men­ter les véri­fi­ca­tions de droits d’ac­cès par Aspect Orien­ted Program­ming. Ce para­digme consiste non pas à écrire du code dans le domaine, mais à déco­rer le code exis­tant.

Jusqu’à PHP 7.4, le seul moyen que nous avions pour implé­men­ter de l’AOP était d’uti­li­ser doctrine/annotations, mais depuis PHP 8.0, nous pouvons utili­ser les Attri­butes désor­mais inté­grés au langage.

Parlons peu, parlons bien, à la suite décou­vrez quelques exemples.

Exemple dans un contrô­leur

Les contrô­leurs dépendent direc­te­ment du frame­work pour lesquels on les écrit. Par consé­quent ils vivent, en théo­rie, dans la couche Infra­struc­ture.

Voici un exemple de contrô­leur utili­sant l’API de Symfony direc­te­ment :

<?php
declare (strict_types=1);

namespace Vendor\App\UserInterface\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class FooController exteds AbstractController
{
    public function doSomething(Request $request): Response
    {
        $this->denyAccessUnlessGranted('Gestionnaire');

        // Ici le code de votre contrôleur.
    }
}

Il existe bien entendu plusieurs autres moyens d’abou­tir à ce résul­tat, comme utili­ser les fire­walls de Symfony par exemple.

Voici son alter-ego utili­sant notre API de véri­fi­ca­tion de droits d’ac­cès :

<?php
declare (strict_types=1);

namespace Vendor\App\UserInterface\Controller;

use MakinaCorpus\AccessControl\AccessRole;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class FooController
{
    #[AccessRole("Gestionnaire")]
    public function doSomething(Request $request): Response
    {
        // Ici le code de votre contrôleur.
    }
}

Vous remarque­rez que malgré un certain nombre d’ef­forts, le contrô­leur reste couplé au compo­sant symfony/http-foundation, mais désor­mais plus au frame­work lui-même, ce qui est déjà un premier pas.

L’uti­li­sa­tion de cet attri­but, nous permet d’an­no­ter notre contrô­leur et de nous débar­ras­ser de la dépen­dance à Symfony\Bundle\FrameworkBundle\Controller\AbstractController et de sa méthode isGranted() et donc de se décou­pler du frame­work.

Exemple de hand­ler de commande

Notre projet utilise un bus de message, qui nous sert à exécu­ter du code de façon asyn­chrone. Pour aller plus loin l’in­té­gra­lité du code, qui effec­tue des écri­tures dans le domaine, est implé­men­tée sous la forme de commande et hand­ler, ce qui rend le projet, sur le papier, complè­te­ment asyn­chrone.

Les commandes et hand­lers appar­tiennent au domaine métier, par consé­quent ils vivent dans la couche Domaine. Il devient alors critique qu’au­cun couplage avec du code exté­rieur ne puisse exis­ter dans ces objets.

Note impor­tante : n’im­porte quel compo­sant logi­ciel de l’in­fra­struc­ture peut envoyer des messages dans le bus, y compris des appli­ca­tions front, ce qui implique que le bus ait un endpoint ouvert pour rece­voir des messages. Dans ce contexte, n’im­porte qui peut envoyer n’im­porte quoi dans notre bus, d’où l’ab­so­lue néces­sité de forcer toutes les commandes du bus à être proté­gées, et donc d’avoir un PEP qui décore le bus.

Nous pouvons écrire un hand­ler pour le bus de message de la façon suivante :

<?php
declare (strict_types=1);

use MakinaCorpus\CoreBus\Attr\CommandHandler;

class FooHandler
{
    #[CommandHandler]
    public function doSomething(SomethingCommand $command): void
    {
        // Ici le code de votre handler.
    }
}

Et le message asso­cié de la façon suivante :

<?php
declare (strict_types=1);

namespace Vendor\App\Domain\Command;

use MakinaCorpus\AccessControl\AccessRole;

#[AccessRole("Client")]
class SomethingCommand
{
    // Vos attributs, constructeur et méthodes
}

Veuillez noter que dans le cadre de ce projet, nous n’uti­li­sons pas symfony/messenger car les choix d’ar­chi­tec­ture pré-datent la version de Symfony où ce dernier a été offi­ciel­le­ment consi­déré comme stable.

Ici, la dépen­dance au code externe MakinaCorpus\AccessControl\AccessRole existe toujours. Cepen­dant, un des aspects inté­res­sant des Attribute de PHP est que le code n’est pas chargé expli­ci­te­ment tant que ce n’est pas demandé expli­ci­te­ment par du code d’auto-confi­gu­ra­tion. Par consé­quent, si vous n’ins­tal­lez pas la dépen­dance, le code reste fonc­tion­nel.

Fonc­tion­na­li­tés

Je ne vais pas détailler l’en­semble des fonc­tion­na­li­tés, mais les plus impor­tantes seule­ment.

Poli­cies

Une policy est l’ins­tance d’une règle métier unitaire, défi­nie par le déve­lop­peur. Chaque Resource peut être déco­rée d’au­tant de poli­cies que dési­rées. Si plusieurs entre en concur­rence sur une-même Resource, le résul­tat des véri­fi­ca­tions de droits d’ac­cès est un ou logique entre toutes ces règles.

Notre library permet de défi­nir les poli­cies avec les modèles suivants :

  • MakinaCorpus\AccessControl\AccessAllOrNothing : indique que toutes les autres poli­cies doivent abou­tir à un allow où le résul­tat global sera deny.
  • MakinaCorpus\AccessControl\AccessAllow : indique que le résul­tat sera arbi­trai­re­ment toujours allow et vous permet de spéci­fier une raison.
  • MakinaCorpus\AccessControl\AccessDelegate : vous permet d’in­diquer le nom d’une autre classe PHP sur laquelle char­ger les poli­cies.
  • MakinaCorpus\AccessControl\AccessDeny : indique que le résul­tat sera arbi­trai­re­ment toujours deny et vous permet de spéci­fier une raison. Ceci est utile par exemple pour proté­ger du code qui est prévu pour être exécuté program­ma­tique­ment par une autre API plutôt qu’être utilisé direc­te­ment par l’uti­li­sa­teur.
  • MakinaCorpus\AccessControl\AccessMethod : vous permet d’in­diquer le nom d’une méthode de l’objet resource à exécu­ter pour déter­mi­ner le droit d’ac­cès. On peut utili­ser ça pour implé­men­ter le modèle ABAC pour Attri­bute Based Access Control et ainsi implé­men­ter la véri­fi­ca­tion d’ac­cès dans notre couche Domaine.
  • MakinaCorpus\AccessControl\AccessPermission PBAC ou Permis­sion Based Access Control: véri­fie que le subject dispose d’une certaine permis­sion. Ce qu’est le subject et comment sont déter­mi­nées les permis­sions qui dépendent alors de l’in­té­gra­tion que vous avez écrite via un permis­sion checker. Cette policy est l’une des rares qui ne dispose pas d’im­plé­men­ta­tion par défaut.
  • MakinaCorpus\AccessControl\AccessResource : permet d’in­diquer un type et un iden­ti­fiant permet­tant de substi­tuer la classe qui porte cet attri­but par un objet chargé par un resource loader en tant que resource pour les véri­fi­ca­tions d’ac­cès.
  • MakinaCorpus\AccessControl\AccessRole RBAC ou Role Based Access Control : véri­fie que le subject dispose d’un certain rôle. Ce qu’est le subject et comment sont déter­mi­nés ses rôles dépendent alors de l’in­té­gra­tion que vous avez écrite via un role checker. Il existe une implé­men­ta­tion par défaut dans l’in­té­gra­tion Symfony qui utilise les rôles de l’uti­li­sa­teur connecté.
  • MakinaCorpus\AccessControl\AccessService permet d’exé­cu­ter la méthode d’un service comme procé­dure de véri­fi­ca­tion d’ac­cès. On peut utili­ser ça pour implé­men­ter le modèle ABAC pour Attri­bute Based Access Control et ainsi implé­men­ter la véri­fi­ca­tion d’ac­cès dans notre couche Domaine.

Méthode et service

À un moment donné lors de la concep­tion de cette API, nous avons eu besoin de délé­guer des véri­fi­ca­tions d’ac­cès au domaine métier. Le modèle qui se rapproche le plus étant l’ABAC pour Attri­bute Based Access Control, où l’iden­tité de la personne n’est plus un facteur pour donner ou non l’ac­cès à une resource, mais une ou des règles métiers qui dépendent de l’état du domaine. Par exemple, auto­ri­ser l’ac­cès à un article de blog que s’il est publié.

Écrire un équi­valent à symfony/expression-language semblait être une voie complexe, et semée d’em­bûches pour une première version. Raison pour laquelle nous avons implé­menté un modèle plus plat, et plus direct au travers des poli­cies AccessMethod et AccessService pour délé­guer l’exé­cu­tion de la véri­fi­ca­tion d’ac­cès au domaine.

Access­Me­thod

Les deux se présentent sous la forme de l’écri­ture d’un appel de méthode dans un chaîne de carac­tères, comme par exemple :

<?php
declare (strict_types=1);

namespace App\Domain\Blog\Entity;

use MakinaCorpus\AccessControl\AccessMethod;
use Symfony\Component\Security\Core\User\UserInterface;

#[AccessMethod("isVisibleFor(subject)")]
class BlogArticle
{
    private bool $published;
    private string $ownerUserId;

    public function isVisibleFor(UserInterface $user): bool
    {
        return $this->published || $user->getUserIdentifier() === $this->ownerUserId;
    }
}

Comme vous pouvez le consta­ter AccessMethod délègue la véri­fi­ca­tion à une méthode présente sur la resource elle-même.

Consi­dé­rons le code suivant dans notre PEP dans le contexte d’une appli­ca­tion Symfony :

<?php
declare (strict_types=1);

namespace App\Infra\Blog\AccessControl;

use App\Domain\Blog\Entity\BlogArticle;
use MakinaCorpus\AccessControl\Authorization;

class SomeServiceAccessControlDecorator implements SomeService
{
    public function __construct(
        private SomeService $decorated
        private Authorization $authorization
    ) {
    }

    /**
     * {@inheritdoc}
     *
     * Method from SomeService
     */
    public function doSomethingWithArticle(BlogArticle $article): mixed
    {
        if (!$this->authorization->isGranted($article)) {
            throw new \DomainException("You shall not pass.");
        }

        return $this->decorated->doSomethingWithArticle($article);
    }
}

La méthode isGranted() va succes­si­ve­ment :

  • Trou­ver notre policy AccessMethod.
  • Cher­cher les subjects dans l’en­vi­ron­ne­ment : comme nous sommes dans Symfony, elle trou­vera au moins une instance de Symfony\Component\Security\Core\User\UserInterface.
  • Vali­der la syntaxe de la chaîne isVisibleFor(subject) et jeter une excep­tion si elle est inva­lide.
  • Puis véri­fier que la méthode isVisibleFor() peut être appe­lée sur l’ins­tance de BlogArticle.
  • Itérer sur tous les subjects et appe­ler la méthode avec le premier dont le typage corres­pond.

Notez que lever les ambi­guï­tés, vous pouvez préci­ser le nom des para­mètres de la méthode appe­lée. Ici, nous passons le Subject du contexte au para­mètre $user de la méthode isVisibleFor() ; vous pouvez écrire :

#[AccessMethod("isVisible(user: subject)")]

Notez que vous pouvez passer arbi­trai­re­ment des attri­buts de l’objet subject ou resource en para­mètre, imagi­nons que nous écri­vions la méthode isVisibleFor() de la sorte pour décou­pler le frame­work Symfony de votre domaine métier :

    public function isVisibleFor(string $username): bool
    {
        return $this->published || $username === $this->ownerUserId;
    }

Vous auriez pu alors écrire :

#[AccessMethod("isVisible(username: subject.id)")]

Ou encore :

#[AccessMethod("isVisible(username: subject.getId)")]

Atten­tion, la réso­lu­tion des attri­buts ressemble beau­coup à celle de Twig, mais un peu diffé­rente :

  • Elle cherche si une méthode public, protected ou private avec un nom iden­tique existe.
  • Si cette méthode existe, et n’a aucun para­mètre ou que des para­mètres option­nels, elle l’ap­pelle et retourne le résul­tat.
  • Si l’ap­pel échoue, ou si la méthode n’existe pas, elle cherche si une propriété public, protected ou private de la classe existe avec ce nom, et retourne sa valeur.
  • Si rien n’est trouvé, le PDP va lancer une excep­tion.

La diffé­rence fonda­men­tale est que ce compo­sant ne cherche pas de getter ou hasser en tentant de conver­tir id en getId par exemple.

Access­Ser­vice

Pour utili­ser un service au lieu d’une méthode, le fonc­tion­ne­ment est simi­laire, mais vous devez commen­cer par écrire le service qui contient la méthode à appe­ler, de la sorte :

<?php
declare (strict_types=1);

namespace App\Domain\Blog\AccessService;

use App\Domain\Blog\Entity\BlogArticle;

class BlogArticleAccessService
{
    public function isVisibleFor(BlogArticle $article, string $username): bool
    {
        return $article->isPublished() || $article->getOwnerUserId() === $username;
    }
}

Ensuite, si vous utili­sez Symfony, enre­gis­trez simple­ment ce service dans le contai­ner en lui appo­sant le tag access_control.service.

Vous pour­rez ensuite réécrire la classe BlogArticle de la sorte :

<?php
declare (strict_types=1);

namespace App\Infra\Blog\Entity;

use MakinaCorpus\AccessControl\AccessService;

#[AccessMethod("BlogArticleAccessService.isVisibleFor(article: resource, username: user.getId)")]
class BlogArticle
{
    private bool $published;
    private string $ownerUserId;
}

Notez ici que :

  • On rajoute un para­mètre, le para­mètre resource. Dans cette API, le para­mètre nommé resource sera toujours l’objet passé à la méthode isGranted(), celui dont la classe porte les Attribute qui défi­nissent nos poli­cies.
  • Pour vous simpli­fier la vie, par défaut l’in­té­gra­tion à Symfony vous permet d’ap­pe­ler le service en utili­sant son class local name, soit ici BlogArticleAccessService, mais vous pouvez cepen­dant utili­ser son FQCN pour Fully Quali­fied Class Name, soit ici App\Domain\Blog\AccessService\BlogArticleAccessService.
  • La syntaxe change un peu, vous devez préfixer le nom de la méthode par le nom du service, par exemple : SomeService.checkThisOrThat(foo: subject, bar: resource)

Inté­gra­tion dans votre projet

Cette biblio­thèque a été conçue pour être faci­le­ment inté­grée et utili­sée dans votre propre code, votre propre projet. C’est d’ailleurs pour cette raison que l’in­té­gra­tion Symfony est volon­tai­re­ment pauvre.

Pour créer un PEP sur un bus de message, vous pouvez procé­der de la sorte en utili­sant le pattern déco­ra­teur.

Imagi­nons que vous utili­sez un bus de message dont l’in­ter­face est la suivante :

<?php

declare(strict_types=1);

namespace SomeUberCommandBus;

interface CommandBus
{
    public function dispatchCommand(object $command): mixed;
}

Vous pouvez écrire un déco­ra­teur de la sorte :

<?php

declare(strict_types=1);

namespace Vendor\App\Infra\CommandBus;

use MakinaCorpus\AccessControl\Authorization;
use SomeUberCommandBus\CommandBus;

class AccessControlCommandBusDecorator implements CommandBus
{
    public function __construct(
        private Authorization $authorization,
        private CommandBus $decorated
    ) {
    }

    /**
     * {@inheritdoc}
     */
    public function dispatchCommand(object $command): mixed
    {
        if (!$this->authorization->isGranted($command)) {
            throw new \Exception("Vous ne passerez pas.");
        }

        return $this->decorated->dispatchCommand();
    }
}

Vous pouvez utili­ser le service MakinaCorpus\AccessControl\Authorization où vous le souhai­tez, et déter­mi­ner quels sont les PEP qui vous concernent dans votre appli­ca­tion.

Bundle Symfony

Le paquet dispose d’un bundle symfony compa­tible avec Symfony 5.4 et 6.0, qui apporte les fonc­tion­na­li­tés suivantes :

  • Auto-confi­gu­ra­tion des compo­sants.
  • Si les compo­sants symfony/security-* sont instal­lés et confi­gu­rés, un subject loca­tor et un role checker utili­sant les utili­sa­teurs et leurs rôles de Symfony sont acti­vés.
  • La véri­fi­ca­tion des droits d’ac­cès est enre­gis­trée auto­ma­tique­ment sur les contrô­leurs.
  • Un service MakinaCorpus\AccessControl\Authorization utili­sable dans votre propre code.

Design logi­ciel

Nous n’avons pas encore répondu à une des problé­ma­tiques expri­mée plus haut, qui est : comment décou­pler le code du domaine du frame­work, tout en utili­sant l’ou­tillage qu’il nous four­nit ?

Ou plus concrè­te­ment : comment utili­ser la gestion des rôles de Symfony alors que nous n’uti­li­sons plus son API direc­te­ment ?

La réponse à cette ques­tion devient triviale dès lors que vous avez compris comment fonc­tionne cette biblio­thèque, et c’est que nous allons décrire main­te­nant.

Il est impor­tant de noter que bien qu’étant four­nie avec une inté­gra­tion Symfony, cette biblio­thèque peut fonc­tion­ner dans une stan­da­lone setup et ne requière aucune dépen­dance à l’ex­cep­tion de makinacorpus/profiling et psr/log, qui sont là pour instru­men­ter.

Concepts

Pour fonc­tion­ner, ce compo­sant logi­ciel défi­nit un certain nombre de concepts qu’il va ensuite mani­pu­ler pour véri­fier les droits d’ac­cès.

Policy et Policy Loader

Une policy est une direc­tive, qui contient la règle − métier ou non − expri­mée par le déve­lop­peur pour mener la véri­fi­ca­tion de droits d’ac­cès.

On retrouve touts les modèles de poli­cies possibles décrits plus haut dans les fonc­tion­na­li­tés, elles sont maté­ria­li­sées par les attri­buts PHP 8.0 décrits. Une policy est une instance d’un de ces attri­buts, qui porte avec elle en mémoire les données métiers pour faire la véri­fi­ca­tion.

Subject et Subject Loca­tor

Un subject repré­sente l’uti­li­sa­teur connecté, une de ses iden­ti­tés, ou n’im­porte quel objet arbi­traire. Il reste volon­tai­re­ment dénué de type et de défi­ni­tion stricts, car il peut varier selon le contexte d’uti­li­sa­tion. Nous consi­dé­rons toujours que plusieurs subjects coexistent : celui utilisé pour les véri­fi­ca­tions des droits d’ac­cès sera déter­miné par le typage demandé de la policy exécu­tée. Dans le cas où le typage attendu ne peut être déter­miné, tous seront testés.

Pour trou­ver ces subjects dans le contexte d’exé­cu­tion, un compo­sant appelé le subject loca­tor existe. C’est une inter­face simpliste qui est triviale à implé­men­ter pour votre frame­work ou projet.

Cette biblio­thèque va toujours consi­dé­rer que les iden­ti­tés peuvent être multiples, et par consé­quent travaillera toujours sur une collec­tion de subjects.

Resource et Resource Loca­tor

La resource repré­sente l’en­tité à laquelle le subject accède, qui peut être n’im­porte quel objet arbi­traire.

La resource est par défaut l’ins­tance sur laquelle notre PDP va cher­cher les Attri­bute PHP. Cepen­dant, par un jeu de confi­gu­ra­tion un peu plus avancé en utili­sant l’at­tri­but AccessResource, celle-ci peut être un tout autre objet, cas dans lequel il fera appel à une chaîne de resource loca­tors qui peut être implé­men­tée très faci­le­ment.

Role Checker

Un rôle est une simple chaîne de carac­tères qui repré­sen­te… un rôle. Cette API ne donne pas d’autre défi­ni­tion au mot rôle qu’une simple chaîne de carac­tères.

L’ap­par­te­nance ou non à un ou des rôles est déter­mi­née par les implé­men­ta­tions de role checker. C’est une inter­face simpliste extrê­me­ment facile à implé­men­ter pour votre frame­work ou projet.

Permis­sion Checker

Une permis­sion est une simple chaîne de carac­tères qui repré­sen­te… une permis­sion. Cette API ne donne pas d’autre défi­ni­tion au mot permis­sion qu’une simple chaîne de carac­tères.

La déten­tion ou non d’une ou plusieurs permis­sions est déter­mi­née par les implé­men­ta­tions de permis­sion checker. C’est une inter­face simpliste extrê­me­ment facile à implé­men­ter pour votre frame­work ou projet.

Le permis­sion checker est le seul compo­sant de cette API qui ne dispose pas d’im­plé­men­ta­tion.

Service loca­tor

Un service est une instance qui dispose de méthodes qui peuvent être appe­lées. Un service peut être vrai­ment n’im­porte quel objet, ou classe portant des méthodes statiques. Pour cette API, un service est une chaîne de carac­tères qui sert à iden­ti­fier une instance.

Le service loca­tor est un compo­sant de cette API qui à partir de l’iden­ti­fiant va cher­cher à trou­ver ou créer l’ins­tance du service. Une implé­men­ta­tion par défaut existe et utilise le service contai­ner de Symfony pour trou­ver ces services.

Method execu­tor

Ce compo­sant est un détail interne de l’im­plé­men­ta­tion et n’est jamais exposé dans l’API, il s’agit d’un compo­sant discret, mais impor­tant. Lorsque vous défi­nis­sez une policy utili­sant AccessMethod ou AccessService, ce compo­sant va, à partir du service trouvé ou de la resource véri­fiée :

  • Parser et véri­fier la chaîne four­nie en entrée.
  • Cher­cher une méthode publique, qui peut être exécu­tée sur l’objet en ques­tion, dont le nom corres­pond à celui donné dans la chaîne four­nie en entrée.
  • Avec les infor­ma­tions de contexte, les para­mètres addi­tion­nels passés à la méthode isGranted() et autres infor­ma­tions aggré­gés (le subject et la resource) va construire une table de corres­pon­dance entre les para­mètres du contexte et les para­mètres de la méthode trou­vée. Ce tableau de corres­pon­dance tient compte du nom, mais aussi du type PHP des para­mètres.
  • Pour ensuite abou­tir à l’exé­cu­tion de la méthode, ou à une erreur si le tableau de corres­pon­dance des para­mètres n’a pas pu être complété.

Arti­cu­la­tion

Premiè­re­ment, tout le métier de véri­fi­ca­tion même des droits d’ac­cès est inté­gré à un unique compo­sant séggrégé derrière l’in­ter­face MakinaCorpus\AccessControl\Authorization. Une implé­men­ta­tion unique existe, il a été fait le choix de l’abs­traire via une inter­face pour masquer cette implé­men­ta­tion au déve­lop­peur qui l’uti­lise. Cet objet est le PDP pour Policy Deci­sion Point, c’est à dire l’object qui évalue concrè­te­ment les poli­cies dans le contexte d’exé­cu­tion et émet un allow ou un deny.

Ce compo­sant dispose de réfé­rences vers chacun des compo­sants suivants:

  • policy loader
  • subject loca­tor,
  • resource loca­tor,
  • permis­sion checker
  • role checker

Et chacune de ces réfé­rences est une chaîne d’im­plé­men­ta­tions qui elles-mêmes vont toutes être inter­ro­gées jusqu’à ce que l’une réponde.

Ce compo­sant va, dans l’ordre:

  • Char­ger toutes les poli­cies de l’objet passé en para­mètre en utili­sant le policy loader.
  • Trou­ver le sujet, en utili­sant le subject loca­tor, s’il existe.
  • Si c’est expli­cité par une policy, essayer de char­ger une resource diffé­rente en utili­sant le resource loca­tor.
  • Inter­pré­ter une à une les poli­cies jusqu’à ce qu’une réponde allow.

La réalité est un petit peu plus complexe que cela, car un certain nombre de déci­sions sont prises par confi­gu­ra­tion, telle que le compor­te­ment en cas d’ab­sence de poli­cies, ou quel conscen­sus doit être adopté si il y a plus de Deny que de Allow. Mais aller plus en détails n’est pas le sujet de cet article.

Conclu­sion

Ce petit micro-compo­sant était au départ conçu pour rester dans le projet dont il est issu. Après plus de deux ans de déve­lop­pe­ment, bien des choses sont arri­vées, y compris de nouvelles fonc­tion­na­li­tés de plus en plus avan­cées, et de nouveaux projets ayant le même besoin. Sa ré-utili­sa­tion en interne a contri­bué à sa stabi­li­sa­tion, au point que nous ayons décidé d’en faire une biblio­thèque PHP Open Source.

Pour résu­mer, ce compo­sant est l’im­plé­men­ta­tion d’un PDP pour Policy Deci­sion Point fourni avec une inter­con­nexion mini­male à Symfony via un PEP pour Policy Enfor­ce­ment Point qui par défaut est simple­ment bran­ché sur l’exé­cu­tion des contrô­leurs. Il se veut léger, indé­pen­dant de tout frame­work mais surtout facile à étendre.

Nous utili­sons à ce jour ce compo­sant dans trois divers projets sans surprises.

Si vous souhai­tez essayer, rendez vous sur https://packa­gist.org/packages/maki­na­cor­pus/access-control.

Si vous souhai­tez regar­der à quoi ça ressemble à l’in­té­rieur, ou remon­ter des bugs, nous vous accueille­rons avec plai­sir sur https://github.com/maki­na­cor­pus/php-access-control.

Formations associées

Formation Symfony

Formation Symfony Initiation

Paris Du 28 au 30 mai 2024

Voir la formation

Formations Outils et bases de données

Formation sécurité web

Paris Du 27 au 29 février 2024

Voir la formation

Actualités en lien

Image
Encart blog DBToolsBundle
21/03/2024

L’ano­ny­mi­sa­tion sous stéroïdes avec le DBTools­Bundle

Le DbTools­Bundle permet d’ano­ny­mi­ser des tables d’un million de lignes en seule­ment quelques secondes. Cet article vous présente la métho­do­lo­gie mise en place pour arri­ver à ce résul­tat.

Voir l'article
Image
Encart Article Symfony Pierre
13/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (1/2)

Pourquoi et comment avons nous fait le choix de faire évoluer la concep­tion de nos projets Symfony, et quelles erreurs avons-nous faites ?

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus