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

Toulouse Du 1 au 3 octobre 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