Skip to content

Gestion d'accès - Policies

Résumé

La gestion d'accès s'appuie sur des policies. Le modèle s'inspire du modèle de gestion d'accès d'AWS (avec toutefois ne nombreux écarts sur lesquels nous reviendrons plus tard) ainsi que certains concepts en place sur cerbos.

Une policy (littéralement une politique) est un regroupement de règles qui décrivent une autorisation ou un refus en fonction

  • du sujet (user, rôle, groupe),
  • du contexte (modèle, dossier, groupe, tuple, champ),
  • des permissions désirées (access, read, write),
  • des actions désirées (read, open, delete, save, etc.),
  • des transitions d'état souhaitées
  • des opérations souhaitées
  • de conditions

Les règles en question sont appelées statements. Ce sont elles qui implémentent la logique d'autorisation (allow) ou de refus (deny). Il est aussi possible que le statement ne fournisse pas de réponse (none). Cela arrive si les critères de décision (sujet, contexte, permissions demandées, actions demandées ou conditions) ne s'appliquent pas.

Lorsqu'une policy est évaluée, toutes les règles qui la composent sont évaluées. La policy accorde une autorisation lorsqu'au moins un des statements accorde une autorisation et pour autant qu'aucun statement n'indique un refus. La policy refuse donc une autorisation dès qu'une règle signale un refus. Dans les autres cas, c'est-à-dire lorsqu'aucune règle ne s'applique au contexte, la policy ne donne pas d'avis et c'est à l'application de décider si cela doit être interprété comme une autorisation ou un refus.

Sur une application utilisant les policies, la gestion des accès fonctionne sur le principe du refus implicite. Cela signifie que l'accès à une ressource ou une action est systématiquement refusé tant qu'il n'y a pas de policy qui l'autorise. À noter qu'à niveau de priorité égal, un refus explicite peut également annuler une autorisation accordée au préalable. Cela signifie que si une policy accorde une autorisation mais qu'une autre de même priorité la refuse, c'est cette dernière qui l'emporte. La hiérarchie entre les policies est établie selon deux axes (structure ou priorité). Ces aspects sont décrits plus bas dans ce document.

Sur une application n'utilisant pas de policies, les accès sont autorisés à tous les utilisateurs.

Évaluation des policies par le moteur

Les policies sont prises en compte lors de différents traitements par le moteur:

  1. Lors de la création de session, le moteur vérifie qu'il existe au moins une policy qui autorise un accès (via une permission access, read, ou write). Un code d'erreur HTTP 403 est retourné si ce n'est pas le cas.
  2. Lors du traitement d'une commande, le moteur vérifie que le sujet est en droit d'invoquer l'action demandée. Pour ce faire, le moteur effectue une recherche d'autorisation parmi toutes les policies applicables qui concernent l'action en question. L'action est autorisée si au moins une policy l'autorise et qu'aucune ne l'interdit.
  3. Lors de la construction de l'arbre de sortie les policies sont également évaluées dans le but de déterminer les permissions (read et write) et les actions autorisées. Ces informations apparaissent dans l'arbre de sortie via un élément authorizations. La feuille de style qui met en forme l'écran peut donc s'appuyer sur cet élément pour afficher/masquer/griser les éléments de l'interface.

    En mode debug, des attributs préfixés par le namespace ch.epilogic.ewt.debug sont ajoutés (ils ne sont pas présents pour les autres modes de fonctionnement). Ces éléments donnent une indication sur la policy (et son niveau de priorité) qui a permis de déterminer le type d'autorisation, ce qui aide grandement au debuggage des policies.

Types de policies

Le modèle de policies AWS prévoit plusieurs types de policies https://docs.aws.amazon.com/fr_fr/IAM/latest/UserGuide/access_policies.html. Dans Ewt, on ne retient que les deux principaux types (du moins pour le moment):

  • les policies de ressource : policy associée à une ressource et accordant une autorisation à un sujet (utilisateur, groupe ou rôle)
  • les policies d'identité : policy associée à un utilisateur, un groupe d'utilisateurs ou un rôle et donnant l'accès à une ressource

Les policies d'identités sont à définir dans le sous-dossier policies de l'application.

Les policies de ressource peuvent être définies inline dans la descript ou au niveau de fichiers XML placés dans le sous-dossier policies de l'application. Ces derniers doivent avoir l'extension .policy ou .xml (les autres types de fichiers sont ignorés, par contre l'arborescence est parcourue récursivement, donc les fichiers présents dans les sous-dossiers sont également traités).

Les policies inline et les policies définies au niveau des fichiers externes peuvent être référencées au niveau de la description.

Remarque sur le format

Dans AWS, les policies sont définies au format JSON. Dans Ewt les policies sont décrites au format XML car celui-ci autorise les retours ligne, ce qui le rend plus adapté à l'écriture de conditions sous la forme de scripts ou de requêtes SQL multilignes.

Resource policy

Une policy de ressource indique les droits rattachés à une ressource donnée. Les statements doivent donc décrire les autorisations et refus d'accès à cette ressource pour les différentes populations de sujets susceptibles d'utiliser l'application.

Les policies de resource sont vérifiées récursivement: si aucune policy de ressource n'est définie pour un champ, on s'appuie sur la policy définie pour son tuple parent, puis pour son groupe, puis son modèle.

Identity policy

Une policy d'identité décrit les droits que possède un utilisateur, un groupe d'utilisateur ou un rôle sur une ou plusieurs ressources.

Notons que l'implémentation des policies d'identité dans Ewt varie de celle d'AWS. En particulier, chaque policy d'identité doit indiquer à quelle catégorie de sujet elle s'applique. Cela se fait au moyen d'une entrée appliesTo dans la policy : on y décrit le/les users concernés, le/les rôles concernés et/ou le/les groupes concernés.

Permissions

Les permissions gèrent l'accès aux différentes ressources de l'application (dossier, tuple, champ, etc.) ainsi que l'application elle-même. La version actuelle prévoit 3 types de permissions:

  • read : Permet de lire le contenu d'un élément mais pas de le modifier.
  • write : Permet de lire et de modifier le contenu d'un élément.
  • access : Cette permission gère l'accès à l'application (et uniquement à l'application). Elle permet donc d'indiquer si un sujet est autorisé à utiliser l'application ou non. La requête sera rejetée avec un code HTTP 403 si cette permission ne peut pas être vérifiée. Notez que les permissions read et write incluent implicitement la permission access. Cela signifie qu'il est possible de se passer de cette permission lorsque la policy accorde un allow sur une permission read ou write.

    À l'inverse, un refus de la permission access entraîne automatiquement un refus des permissions read et write.

Permission access

La permission access est incluse dans les permissions read et write. Cela signifie qu'un sujet reçoit implicitement la permission access lorsqu'une policy accode un read ou un write. On est donc en droit de se demander à quoi sert la permission access.

Les premières versions du moteur Ewt n'intègraient pas la permission access. À la place, on se rabattait sur read. Elle a été ajoutée afin d'offrir un moyen de traiter différemment les comptes standards (les comptes appartenant aux utilisateurs) et les comptes techniques (les comptes utilisés par des services web). Ces derniers n'ont généralement pas besoin d'accéder aux pages HTML de l'interface utilisateur.

L'utilisation de access offre une granularité plus fine que read car elle permet d'accorder l'accès à l'application au compte technique (ce qui l'autorise à invoquer un service par exemple) tout en garantissant que le compte ne pourra pas accéder à l'interface utilisateur.

Actions

Les actions (open, save, close, delete, etc.) désignent les types d'actions que le client peut envoyer au moteur via une commande. Il est important de noter que les actions portent uniquement sur les groupes, les dossiers ou sur l'application, alors que les permissions portent sur tous les types d'objets (dossier, tuple, champ, etc.), ainsi que sur l'application dans le cas de permission access.

La version actuelle implémente les actions suivantes:

  • addTuple (version 1.0)
  • admin (version 1.0)
  • arrange (version 1.0)
  • close (version 1.0)
  • create (version 1.0)
  • clone (version 1.0)
  • delete (version 1.0)
  • delTuple (version 1.0)
  • dummy (version 1.0)
  • open (version 1.0)
  • reset (version 1.0)
  • save (version 1.0)
  • script (version 1.0)
  • setLocale (version 1.0)
  • setState (version 1.0)
  • setStyle (version 1.0)
  • setView (version 1.0)

Le numéro de version indiqué est le numéro de la version du moteur de policies prenant en charge l'action donnée. Nous revenons plus bas sur le rôle du numéro de version des policies.

Les actions peuvent être inscrites avec une casse différente : celle-ci est ignorée lors du chargement de la policy.

Transitions

Les transitions sont des références à des transitions d'états définies au niveau des modèles d'états (sous-dossier states d'une application).

Les règles transition fonctionnent sur le même principe que les règles action.

Opérations

Les opérations représentent des ensembles de traitements propres à l'application. Il ne s'agit pas d'une notion prise en charge par Ewt, mais uniquement une facilité mise à disposition pour vérifier des droits sur la base d'un élément personnalisé.

Par exemple, une opération pourrait être "launchDatabaseClean". Le traitement sous-jacent de cette opération est propre à l'application et n'est pas une fonctionnalité d'Ewt. L'idée ici est de pouvoir demander à Ewt si une opération "launchDatabaseClean" a été considérée comme possible dans un contexte donné.

Infos de base

La policy est définie au moyen des éléments suivants:

  • attribut name : Nom de la policy formé de caractères alphanumériques et ne commençant pas par un chiffre. Ce nom sert à identifier la policy. Il peut être repris ensuite pour référencer la policy, par exemple au niveau de la descript.

  • attribut type : Type de policy; peut contenir les valeurs resource ou identity

  • attribut priority : Facultatif. Cet attribut permet de définir la priorité de la policy. Voir le chapitre Hiérarchie des policies et plus spécifiquement le point Hiérarchie par priorité pour une description plus détaillée.

  • attribut version : Facultatif. Numéro de version selon lequel la policy doit être traitée. En pratique, le numéro de version sert à indiquer à Ewt sur quelle version du modèle de policy il doit s'appuyer. Il s'agit d'un garde-fou principalement destiné à l'interprétation de l'élément wildcard.

    Pour comprendre le rôle la version, prenons l'exemple suivant: on définit un statement qui est valable pour toutes les actions en notant <action>*</action>. Le périmètre des actions concernées peut varier au fil du temps, en particulier si une version future du moteur implémente une nouvelle action : dans ce cas, la nouvelle action sera de facto comprise dans le statement, ce qui peut représenter un risque selon la nature de cette nouvelle action.

    Pour éviter que l'expression wildcard n'englobe des fonctions non connues et non souhaitées, on indiquera au moyen de l'attribut version la version du moteur de traitement des policies à utiliser. Actuellement seule la version "1.0" est disponible, mais cela est susceptible d'évoluer à l'avenir.

    En l'absence de numéro de version, le moteur considère que la policy est compatible avec la dernière version en date du modèle. Le même résultat est également possible en spécifiant la valeur latest.

  • attribut disabled : Facultatif. Cet attribut permet de désactiver une policy. Dans ce cas, la policy n'est pas chargée par le moteur.

    Cet attribut peut être pratique pendant développement pour désactiver provisoirement une policy.

  • élément description : Facultatif. Cet élément ne joue aucun rôle. Il permet de saisir une description de la policy, de son rôle, etc.

  • élément appliesTo (uniquement requis pour les policies d'identité) : Filtre indiquant à quel genre de sujet la policy d'identité s'applique. Il permet de filtrer les user, role et/ou group auxquels la policy s'applique.

    L'élément sert essentiellement pour l'optimisation de traitement en interne au moteur. Lors de l'évaluation des policies, l'application ne vérifiera que les policies qui s'appliquent réellement à l'utilisateur connecté.

  • élément(s) statement : Règle de d'autorisation ou de refus (voir ci-dessous)

Les statements

Les statements sont des règles référencées par la policy. Ils constituent le coeur des policies.

Un statement n'est évalué que si le contexte déterminé par le sujet, la ressource et/ou les conditions est vérifié. En outre, certaines évaluations de policies s'effectuent dans le cadre d'un contrôle de permission et/ou d'action. Les éléments correspondants sont donc également pris en compte dans ce cas.

Les statements d'une policy sont généralement constitués de:

  • attribut effect : Cet élément peut prendre les valeurs allow ou deny. Il s'agit de la décision retournée par le statement si ce dernier est applicable au contexte actuel (établi en fonction du contexte de dossier et du sujet connecté).

  • attribut level : Facultatif. Cet attribut permet d'indiquer au moteur que le statement n'est à évaluer que pour un élément d'un niveau donné. Cela permet par exemple d'indiquer au moteur que le statement n'est à évaluer que pour un élément de niveau "tuple".

    L'attribut peut prendre les valeurs suivantes:

    model
    Désigne le niveau "modèle".
    document (ou doc)
    Désigne le niveau "dossier", soit un modèle + un identifiant de dossier.
    group
    Désigne le niveau "groupe", soit un modèle + un identifiant de dossier + un nom de groupe
    column
    Désigne le niveau "colonne", soit un modèle + un identifiant de dossier + un nom de groupe + un nom de champ
    Ce niveau est particulier et n'existe pas dans les autres parties d'une application Ewt. De plus, il n'est utilisable que dans le cas de groupes multis. Comme son nom l'indique, il désigne une colonne de groupe. On peut l'associer à l'entête de colonne : il ne désigne pas des instances de champs, mais une colonne de la table.
    tuple
    Désigne le niveau "tuple", soit un modèle + un identifiant de dossier + un nom de groupe + un identifiant de tuple
    field
    Désigne le niveau "champ", c'est-à-dire un contexte complet
    none
    Désigne un contexte vide

    Voici un exemple de statement défini pour le niveau "tuple". Ici le but de ce statement est de désactiver l'action delTuple d'un dossier ayant un statut autre que "creation" et sur chaque ligne de commentaire dont l'utilisateur courant n'est pas l'auteur.

    1
    2
    3
    4
    5
    6
    7
    8
    <statement effect="deny" level="tuple">
        <resource>ticket.commentaires</resource>
        <action>delTuple</action>
        <condition mode="state" reverse="true">creation</condition>
        <condition require="tupleId"><![CDATA[
            return #idAuteur != $session.getPersistentObject("user-docId");
        ]]></condition>
    </statement>
    

    La condition de la ligne 4 filtre l'état du dossier et la condition de la ligne 6 vérifie que l'auteur du commentaire n'est pas l'utilisateur courant.

    Ici le level "tuple" est nécessaire pour plusieurs raisons:

    1. L'action delTuple se rapporte exclusivement à un tuple
    2. La condition de la ligne 6 fait référence à #idAuteur. Évaluée en-dehors du niveau "tuple", cette expression est soit null, soit un tableau de valeurs. En effet, si on se place au niveau "group", l'expression s'applique au niveau du groupe, et elle porte donc sur tous les tuples du groupe, raison pour laquelle elle contient un tableau de valeurs.
  • élément subject : Uniquement pour les policies de ressources.

    Cet élément permet de décrire les règles d'identification de sujets sur lesquels s'applique le statement. On définit la population au moyen de trois types d'éléments: user, role et/ou group. Chaque élément peut figurer entre 0 et n fois. Il est possible de regrouper plusieurs valeurs d'un même type (par exemple plusieurs roles) en une seule en les séparant par une virgule (par exemple <role>EWT_1,EWT_2,EWT_3</role>)

    Comme pour les actions décrites plus haut, il est possible d'ajouter un attribut except pour spécifier des exceptions.

    Si aucun sujet n'est indiqué, le statement est évalué pour tous les utilisateurs.

  • élément(s)resource : Uniquement pour les policies d'identité.

    Cet élément permet d'indiquer la ou les ressources pour lesquelles le statement s'applique. Les ressources sont à référencer à l'aide d'une expression de contexte.

    Il est possible de référencer n'importe quelle ressource en indiquant une valeur wildcard *.

    Il est possible de spécifier une chaîne de caractères si on ne souhaite référencer qu'une seule valeur.

    Attention, la valeur "*" n'est pas équivalente à la valeur "".

    La valeur "*" désigne n'importe quel contexte non vide. Cela signifie qu'il faut qu'un dossier soit ouvert pour que le statement s'applique.

    La valeur vide (<resource/>) désigne aucun contexte. La valeur permet de représenter le cas où l'on est hors dossier. Ainsi, la référence <resource>,*</<resource> permet d'englober à la fois le contexte "hors dossier" et le contexte "dossier ouvert", indépendamment de son modèle.

    Il est possible de ne pas spécifier de ressource au niveau d'un statement. Dans ce cas, le moteur n'effectue pas de contrôle de contexte. Cela signifie qu'il évalue le statement dans tous les cas, indépendamment du contexte actuel.

    Gestion des exceptions

    La question des exception au niveau des ressources est délicate et elle n'est pas recommandée car elle peut facilement amener à des situations difficiles à comprendre et à debugger.

    Héritage

    En premier lieu, il faut garder à l'esprit que les ressources héritent leurs autorisations de leurs parents. Par conséquent, si une exception est définie pour un niveau inférieur à une autre ressource du même statement, elle sera sans effet. Pour illustrer cela, prenons l'exemple suivant

    1
    2
    3
    <resource>ticket</resource>
    <resource except="true">ticket.base.notifications</resource>
    <resource except="true">ticket.commentaires.btn1</resource>
    

    Ici la ressource ticket est à un niveau bien supérieur que les exceptions. Celles-ci n'appliqueront pas l'effet du statement, mais hériterons de l'effet du statement appliqué à la ressource ticket. Au final, cela rend les exceptions sans effet.

    Comme indiqué plus haut, pour qu'une exception de ressource soit prise en compte, il faut qu'elle référence une autre ressource de même niveau. Par conséquent, les ressources devraient être déclarées ainsi pour que cela ait un sens:

    1
    2
    3
    4
    <resource>ticket.base.*</resource>
    <resource except="true">ticket.base.notifications</resource>
    <resource>ticket.commentaires.*</resource>
    <resource except="true">ticket.commentaires.btn1</resource>
    

    Cette notation est fastidieuse, c'est pourquoi dans une telle situation, il devient préférable de définir une policy supplémentaire qui vient imposer l'exception au moyen d'un niveau de priorité supérieur.

  • élément(s) permission : Cet élément permet de déclarer les permissions que l'on souhaite autoriser/refuser. Les permissions sont access, read ou write. Pour spécifier plusieurs permissions, il est possible de répéter plusieurs fois l'élément permission ou de noter les permissions via un seul élément en séparant les permissions par une virgule, par exemple read,write.

  • élément(s) action : Cet élément permet de déclarer les actions sur lesquelles porte le statement. Les actions possibles (open, save, etc.) sont données plus haut dans ce document. Les actions peuvent être notées via des éléments action distincts, ou regroupés au sein d'une valeur séparée par des virgules.

    Il est possible de désigner "toutes les actions" au moyen de la valeur wildcard *. Vous pouvez également spécifier des exceptions à la liste d'actions. Pour cela, il suffit d'ajouter un attribut except="true" à l'élément action (voir exemple plus bas). Veuillez toutefois lire plus haut l'importance de l'attribut version lorsque vous définissez une règle portant sur toutes les actions.

    1
    2
    3
    4
    5
    6
    <statement effect="deny" version="1.0">
      ...
      <action>*</action>
      <action except="true">open,close,arrange,save</action>
      ...
    </statement>
    
  • élément(s) transition : Cet élément permet de spécifier une ou plusieurs transitions à autoriser/refuser. Les transitions peuvent être notées via des éléments transition distincts, ou regroupés au sein d'une valeur séparée par des virgules.

  • élément(s) operation : Cet élément permet de spécifier une ou plusieurs opérations à autoriser/refuser. Les opérations peuvent être notées via des éléments operation distincts, ou regroupés au sein d'une valeur séparée par des virgules.

    Le terme opération ici ne désigne pas une fonctionnalité offerte par le moteur Ewt, mais un processus propre à l'application. On pourra par exemple se servir de cet élément pour déterminer s'il faut mettre à disposition ou non un bouton au niveau de l'interface utilisateur.

  • blocs d'éléments facet : Il est possible de regrouper des éléments resource, subject, permission, action, transition, operation par blocs. Cette option remplit un rôle pratique : elle permet de regrouper des règles qui sont soumises à une même condition. Cela évite de créer plusieurs statements avec une même condition, ce qui présente un intérêt du point de vue syntaxique (cela évite les répétitions lorsqu'une condition est un script, voire un regroupement de conditions (voir élément conditions plus bas)), mais également une optimisation : cela évite au moteur de devoir évaluer plusieurs fois une même condition.

    Le moteur numérote les facet à partir de 1 selon l'ordre dans lequel ils sont définis. Les éléments définis directement dans statement sont quant à eux regroupés dans un facet portant le numéro 0. Ainsi, les lignes 2 et 3 de l'exemple ci-dessous constituent le facet #0 alors que les lignes 6 et 7 forment le facet #1.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <statement effect="allow">
        <resource>ticket</resource>
        <permission>read,write</permission>
    
        <facet>
            <resource>ticket.commentaires.btnActions</resource>
            <operation>edition</operation>
        </facet>
    </statement>
    
  • élément(s) condition : Cet élément contient une condition supplémentaire qui indique si le statement est applicable ou non. L'élément peut contenir un script, une requête SQL ou une liste d'états. Cette distinction peut être déclarée à l'aide de l'attribut mode. Le détail des différents modes est donné plus bas.

    La condition est censée générer une réponse booléenne (dans le cas du mode state, c'est la correspondance entre l'état actuel du dossier et l'état indiqué en condition qui génère la réponse booléenne). Lorsque la condition ne génère pas de réponse (en particulier pour les modes script et sql) ou une réponse null, le moteur l'interprète comme un false. Il est possible d'indiquer explicitement la valeur par défaut à appliquer dans ce cas avec l'attribut default.

    L'attribut reverse permet de faire fonctionner la condition en mode inversé. Ainsi, en mode inversé, le statement est appliqué lorsque la condition est false. Ce mode est particulièrement pratique avec le mode state lorsqu'un statement doit s'appliquer pour tous les états sauf ceux indiqués.

    Il est possible de spécifier plusieurs conditions pour un même statement. Veuillez lire plus haut la description de l'élément conditions (avec un "s" !) pour comprendre le comportement du moteur en présence de plusieurs conditions.

    Mode script
    En mode script, on s'attend à ce que la valeur fournie en retour soit true ou false. La valeur de l'élément condition est le script que le moteur devra évaluer. Notez qu'il est possible d'utiliser un élément CDATA pour faciliter la syntaxe. Cela évite de devoir échapper les caractères spéciaux du XML:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <condition>
        <![CDATA[
            if (#base.motifPrincipal) {
                switch (#base.motifPrincipal) {
                    case "H-PP":
                    case "H-IG":
                    case "H-DP":
                        return false;
                    default:
                        return true;
            }
            return true; 
        ]]>
    </condition>
    
    Mode sql
    Comme pour le mode script, on s'attend à ce que la valeur fournie en retour (1re colonne de la 1re ligne du resultset) soit true ou false. Les valeurs "true", "1" et "yes" sont interprétées comme true.
    La condition en mode sql utilise la même notation que pour les options ou les valeurs par défaut de la descript : la requête doit être placée dans un élément <value> et les éventuels paramètres dans des éléments <param>. Lorsque ces derniers sont exprimés à l'aide d'une variable, ils doivent avoir un attribut type, comme dans le cas de la descript. L'attribut est optionnel dans le cas d'expression sharp.
    1
    2
    3
    4
    5
    6
    <condition mode="sql" defaultValue="true">
        <value>select MotifPrincipal not in ('H-PP','H-IG','H-DP')
               from ConsultationHOP
               where IdConsultation=?</value>
        <text:param>#idConsultation</text:param>
    </condition>
    
    Mode state
    Une condition utilisant le mode state vérifie que le dossier courant est dans l'un des états déclarés dans la condition.
    La valeur de l'élément condition peut être:
    • une chaîne de caractères : on indiquera le nom de l'état ou des états qui remplissent la condition (utiliser la virgule comme séparateur dans le cas où plusieurs états doivent être spécifiés)
      <condition mode="state">monEtatA,monEtatB</condition>
      
    • un ou plusieurs éléments <state>
      1
      2
      3
      4
      <condition mode="state">
          <state>monEtatA</state>
          <state>monEtatB</state>
      </condition>
      
    • un élément ou plusieurs éléments <states> dont la valeur est une chaîne de caractères indiquant le nom d'un ou plusieurs états séparés par une virgule.
      1
      2
      3
      <condition mode="state">
          <states>monEtatA,monEtatB</states>
      </condition>
      

    Il est possible de mixer entre les éléments <state> et <states>.

    Lorsque la condition fait référence à un ou plusieurs champs de dossier, il est nécessaire d'ajouter un attribut require à l'élément condition pour indiquer au moteur que la condition ne peut s'exécuter que dans le contexte d'un dossier ouvert. L'attribut peut prendre les valeurs suivantes:

    docId (ou docid)
    Cette valeur indique que la condition ne peut s'appliquer que dans le cas où le contexte de la ressource à évaluer possède un identifiant de dossier. Autrement dit, la condition n'est évaluable que dans le contexte d'un dossier ouvert.
    tupleId (ou tupleid)
    Cette valeur indique que la condition ne peut s'appliquer que dans le cas où le contexte de la ressource à évaluer possède un identifiant de tuple. Ce prérequis sert à cibler les tuples de groupes multi. Elle évincera par contre le groupe multi lui-même car celui-ci est une ressource dont le contexte s'exprime au moyen d'une expression sans identifiant de tuple.

    Le fait de spécifier le pré-requis sert à ce que la condition soit évaluée dans le bon contexte. C'est également une manière pour le moteur d'optimiser le traitement des policies et d'éviter l'évaluation de statements non adaptés au contexte courant.

    Il n'est pas nécessaire de spécifier l'attribut pour les conditions utilisant le mode state car le moteur sait déjà que dans ce cas la condition nécessite un dossier ouvert. L'attribut est donc ajouté automatiquement par le moteur.

  • élément conditions: L'élément conditions permet de regrouper plusieurs conditions ensemble et de spécifier un opérateur sur leur valeur de retour.

    Lorsqu'un statement possède plusieurs conditions, il est possible de définir plusieurs éléments condition les uns à la suite des autres. Dans ce cas, le moteur n'appliquera l'effet du statement que si toutes les conditions sont remplies. À l'inverse, le statement sera ignoré dès qu'une condition n'est pas remplie. On peut dire que dans ce cas, le moteur applique un opérateur logique "ET" (ou and) entre les conditions.

    Il peut cependant arriver que l'on veuille appliquer un opérateur "OU" (ou ou) sur les conditions. Dans ce cas, il est nécessaire de définir un élément conditions et de lui ajouter un attribut operator="or". Dans ce cas l'effet associé au statement sera appliqué dès qu'une condition est remplis.

    L'élément conditions supporte donc deux types d'opérateurs:

    and
    Indique que toutes les conditions doivent être remplies pour que le statement s'applique. C'est le comportement par défaut.
    or
    Indique que le statement s'applique dès qu'au moins une condition est vérifiée.

    Voici un exemple de déclaration avec l'opérateur or. Dans cet exemple, on souhaite refuser l'écriture sur tous les champs du groupe ticket.base et bloquer l'action delete lorsque le dossier n'est pas dans l'état "ticketCreation" ou que l'utilisateur courant n'est pas l'auteur du dossier.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <statement effect="deny"> 
        <facet>
            <resource>ticket.base.*</resource>
            <permission>write</permission>
        </facet>
    
        <facet>
            <action>delete</action>
        </facet>
    
        <conditions operator="or">
            <condition mode="state" reverse="true">ticketCreation</condition>
            <condition require="docId"><![CDATA[
                return #idUtilisateur != $session.getPersistentObject("user-docId");
            ]]></condition>
        </conditions>
    </statement>
    

    Discussion de l'exemple ci-dessus

    On notera au passage que le statement utilise deux "facets": l'un pour gérer les permissions et le second pour gérer l'action. Il ne serait pas correct de regrouper les deux facets en un seul car celles-ci ne s'appliquent pas dans le même contexte : la permission write porte ici sur des champs alors que l'action delete, par nature, porte sur le dossier.

Policy pour accès à une application

Les servlets autorisent l'accès à une application lorsque la policy de base autorise au moins une action à l'utilisateur. La policy de base est définie dans la config via la propriété policies.policy.

Exemple de policy de base:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="main" type="identity" priority="-100">
    <description>Cette policy d'identité vérifie le rôle technique. Elle 
        n'accorde l'accès à l'application qu'aux sujets qui possèdent le
        rôle "EWT"
    </description>
    <appliesTo>
        <role>EWT</role>
    </appliesTo>
    <statement effect="allow">
        <permission>access,read</permission>
    </statement>
</policy>

Hiérarchie des policies

Le moteur traite les règles de policies de manière hiérarchique selon deux axes: la hiérarchie de structure et la hiérarchie par priorité.

Hiérarchie de structure

La hiérarchie de structure s'appuie sur la structure de la descript. On peut parler ici d'héritage. Dans Ewt, il existe deux formes d'héritage:

  • l'héritage standard
  • l'héritage ascendant des autorisations (allow)

Héritage standard

Une policy appliquée à un nœud donné, par exemple le nœud <model>, s'applique à toute la structure de ce nœud, donc à ses groupes, ses tuples, ses champs.

Il est possible de définir d'autres policies dans les éléments de la sous-structure (groupes, tuples, champs) mais ces dernières ne peuvent pas contredire des règles définies plus haut. En particulier, elles ne peuvent autoriser des actions qui ont été refusées à un niveau supérieur.

En clair, si une policy impose un deny sur le niveau model, les policies qui accordent un allow sur les champs de ce même modèle seront sans effet car bloqués par le deny du niveau supérieur.

L'inverse est par contre possible : si une policy accorde un allow sur le niveau model, il est bien entendu possible d'imposer un deny sur un champ. Le deny prime sur le allow dans l'héritage standard.

Héritage ascendant

L'héritage ascendant concerne uniquement les permissions avec un effet allow : si une policy accord un allow sur un champ, alors ce allow est également accordé à tous les éléments parents du champ, à condition qu'aucune autre policy ne vienne à l'encontre de ce allow.

Le mécanisme devient logique si on prend un exemple : une policy accord un droit sur un champ. Pour que ce champ soit accessible, il faut bien entendu que le tuple parent soit visible, de même que le groupe parent et le modèle parent. Si le tuple, le groupe et le modèle n'ont pas de policy particulière associée, alors ils reçoivent également le allow pour permettre au champ d'être accessible.

Hiérarchie par priorité

La hiérarchie par priorité offre une solution pour court-circuiter la limitation imposée par la hiérarchie de structure standard, à savoir le fait qu'un niveau inférieur ne peut pas accorder plus d'autorisation que ne le permet le niveau supérieur.

Prenons le cas suivant:
On souhaite définir une policy globale indiquant qu'un dossier est en lecture seule lorsque son statut est "archivé". Pour cela, on définit la policy de ressource au niveau de la descript. En imaginant que tous les modèles possèdent un champ "statut", la policy pourrait être :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="statut" type="resource">
    <description>Cette policy définit les règles d'accès vis-à-vis du statut 
        d'un dossier: on refuse toute action sur un dossier archivé</description>
    <statement effect="deny">
        <subject>
            <role>*</role>
        </subject>
        <permission>write</permission>
        <action>*</action>
        <condition mode="state">refuse,archive</condition>
    </statement>
</policy>

Cependant, on aimerait malgré tout autoriser un administrateur à intervenir sur les champs d'un dossier archivé mais la hiérarchie de structure l'interdit. Pour contourner cette limitation, il est possible d'utiliser la notion de priorité. La priorité est représentée par une valeur numérique rattachée à la policy via l'élément priority.

Une policy peut accorder des droits plus étendus que son parent à la condition qu'elle ait une priorité supérieure ou égale à la policy qui a établi le deny sur une action donnée.

Évaluation des policies

https://docs.aws.amazon.com/fr_fr/IAM/latest/UserGuide/reference_policies_evaluation-logic.html

Exemple de policy d'identité

On souhaite donner accès à une catégorie d'utilisateurs pour qu'ils effectuent une tâche spécifique. Par exemple, on souhaite donner accès aux listes d'articles à des vendeurs pour qu'ils puissent saisir les articles qu'ils veulent mettre en vente. Ces vendeurs ne doivent pouvoir accéder qu'à leur liste d'article et à rien d'autre.

On définit alors une policy d'identité qui interdit l'accès à toute ressource qui n'est pas la liste d'articles du vendeur.

La policy suivante permet d'implémenter cela:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="vendeur" type="identity">
    <description>Cette policy gère les droits des users de niveau 
   'vendeur'</description>
    <appliesTo>
        <role>EWT_3</role>
    </appliesTo>
    <statement effect="deny">
        <description>
            Ce statement bloque la plupart des actions (hormis open, save et 
            script) aux vendeurs</description>
        <action>close,addTuple,admin,setView,create,delete,delTuple,reset,setLocale,setStyle</action>
        <resource>listeArticle</resource>
    </statement>
    <statement effect="deny">
        <description>
            Ce statement refuse l'accès aux vendeurs sur les listes d'articles 
            qui ne sont pas les leurs</description>
        <permission>read,write</permission>
        <resource>listeArticle</resource>
        <condition require="docId">
            return (`${data:idListeArticle}` != ``
                    ? $request.getPrincipal() != 
                        $sql.select(`select Vendeur.Numero
                                     from ListeArticle inner join Vendeur
                                          on Vendeur.idVendeur=ListeArticle.idVendeur
                                     where idListeArticle=?`::T,
                                    [ #idListeArticle ])
                   : false);</condition>
    </statement>
</policy>

Remarque concernant les mises à jour de policy

Lors du développement, on peut être amené à modifier, voire ajouter ou supprimer des policies. Il faut alors demander à l'application d'effectuer un rechargement des policies pour que les modifications soient appliquées. Ce rechargement peut être effectué au moyen de la commande reset.

Cette action a pour effet de forcer un rechargement complet de l'application, de la config, de la descript, des policies, etc. Toutefois elle ne peut pas agir directement sur les objets chargés en mémoire, en particulier les dossiers ouverts. La nuance est peut-être difficile à percevoir. Pour mieux comprendre les choses, voyons comment Ewt organise les éléments.

L'application contient une table de policies. Lorsqu'une policy est appliqué à une ressource (un modèle, un tuple, un champ, etc.), cette dernière conserve en mémoire le nom de la policy en question. La policy en elle-même est décrite au niveau de la table de policies de l'application, mais le nom de la policy est rattaché à la ressource.

Lors d'un reset, la table des policies de l'application est rechargée, mais pas les références par nom qui sont enregistrées au niveau des ressources. Cela fait qu'après un reset, les ressources ont toujours les mêmes listes de noms de policies, dans le même ordre qu'avant le reset.

Par conséquent si le changement concerne purement la policy (ajout d'une action, modification de l'effet, modification des conditions, etc.), alors la nouvelle version de la policy sera bien prise en compte, car la ressource continuera de référencer la policy et celle-ci sera mise à jour.

Par contre si la modification consiste à ajouter une nouvelle policy à une ressource, ou à modifier l'ordre des policies au sein d'une ressource, alors la modification ne se remarquera pas immédiatement car la ressource conservera la liste de noms de policies qu'elle connaît, mais ignorera les nouvelles policies ou le nouvel ordre des policies. La modification ne s'appliquera qu'aux dossiers ouverts après le reset et pas à ceux qui étaient déjà ouverts avant le reset. Pour ces derniers, il faudra fermer puis rouvrir le dossier pour que la modification soit effective.

Contrôles

La mise en place de policies peut être rapidement complexe et peut présenter des pièges. En mode d'exécution "dev", le moteur effectue un certain nombre de contrôles sur les policies et signale les possibles erreurs sous la forme d'avertissement dans le log de l'application lorsqu'il charge celle-ci en mémoire (lors de l'initialisation de l'application et lors des "reset").

Voyons quelques problèmes courants que l'on peut rencontrer avec l'exemple ci-dessous:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- Ce statement n'est pas optimal, ne pas l'utiliser -->
<statement effect="allow">
    <resource>ticket</resource>
    <resource>ticket.base</resource>
    <resource>ticket.commentaires.dateHeure</resource>
    <resource>ticket.commentaires.idAuteur</resource>
    <resource>ticket.commentaires.texte</resource>
    <action>arrange,close,save,script,setLocale,setStyle,setView</action>
    <action>addTuple,create,open</action>
    <permission>read</permission>

    <condition mode="state">ticketCreation</condition>
</statement>

Ce statement présente plusieurs défauts.

  1. Niveaux de ressources

    Le statement ci-dessus mélange des ressources de plusieurs niveaux, dont certains appartiennent aux autres. Ainsi, la référence de ressource ticket vient écraser toutes les autres références de ressources "fine grain". En effet, le fait d'associer la permission read et la resource ticket indique au moteur que l'intégralité des éléments du contexte ticket est en read. Les références aux autres ressources deviennent alors inutiles car ces dernières héritent naturellement les permissions de leur parent.

    La seule option pour éviter cela serait de séparer les règles en deux statements distincts, comme mentionné en introduction, mais présente le désavantage de dupliquer la condition sur les deux statements. Ce n'est pas idéal du point de vue syntaxique et cela oblige le moteur à évaluer deux fois une même condition.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <statement effect="allow">
        <resource>ticket</resource>
        <action>arrange,close,save,script,setLocale,setStyle,setView</action>
        <action>addTuple,create,open</action>
        <condition mode="state">ticketCreation</condition>
    </statement>
    
    <statement effect="allow">
        <resource>ticket.base</resource>
        <resource>ticket.commentaires.dateHeure</resource>
        <resource>ticket.commentaires.idAuteur</resource>
        <resource>ticket.commentaires.texte</resource>
        <permission>read</permission>
        <condition mode="state">ticketCreation</condition>
    </statement>
    

    Pour éviter la répétition, on aura alors recours à des éléments facet, ce qui permet de regrouper tous les éléments dans le même statement et qui évite la répétition de la condition.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <statement effect="allow">
        <facet>
            <resource>ticket</resource>
            <action>arrange,close,save,script,setLocale,setStyle,setView</action>
            <action>addTuple,create,open</action>
        </facet>
        <facet>
            <resource>ticket.base</resource>
            <resource>ticket.commentaires.dateHeure</resource>
            <resource>ticket.commentaires.idAuteur</resource>
            <resource>ticket.commentaires.texte</resource>
            <permission>read</permission>
        </facet>
    
        <condition mode="state">ticketCreation</condition>
    </statement>
    
  2. Mélange d'actions ne portant pas sur le même niveau

    L'exemple mélange des actions qui portent sur un niveau "document" (create, open) et des actions qui concernent un niveau "tuple" (arrange, addTuple, etc.). En outre, il y associe des ressources qui référencent différents niveaux.

    Cela ne fait pas sens car une action delete ne concerne qu'un niveau modèle et non un tuple. Le moteur signalera une possible erreur en présence de statement présentant ces caractéristiques.

  3. Permissions et ressources

    Le fait de mélanger une ressource <resource>ticket</resource> et une permission n'est généralement pas correcte ou pas souhaitée. Le moteur s'attend à ce que la ressource désigne un champ ou qu'elle soit notée avec un suffixe .*. Dans notre cas, cela donnerait

    <resource>ticket.*</resource>
    <permission>read</permission>
    

Exemple

Ce chapitre présente un exemple d'implémentation de gestion de droits sur la base de policies. Pour cet exemple, nous nous appuyons sur une application très basique faisant intervenir 2 modèles:

  • entreprise : contient les informations de base d'une entreprise
  • personne : contient les informations de base d'une personne et une référence vers l'entreprise à laquelle la personne est rattachée
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<models>
    <model name="entreprise" maingroup="info">
        <groups>
            <group name="info" table="Entreprise" type="single" 
                   mainfield="idEntreprise">
                <fields>
                    <field name="idEntreprise" type="hidden" column="idEntreprise"/>
                    <field name="nom" type="text" column="Nom"/>
                    <field name="adresse" type="textarea" column="Adresse"/>
                    <field name="remarque" type="text" column="Remarque"/>
                </fields>
            </group>
        </groups>
        <views>
            <view name="viewDocuments" style="documents">
                <group name="info"/>
            </view>
        </views>
    </model>

    <model name="personne" maingroup="info" mainfield="idPersonne">
        <groups>
            <group name="info" table="Personne" type="single">
                <fields>
                    <field name="idPersonne" type="hidden" column="idPersonne"/>
                    <field name="nom" type="text" column="Nom"/>
                    <field name="prenom" type="text" column="Prenom"/>
                    <field name="email" type="email" column="EMail"/>
                    <field name="idEntreprise" type="text" column="idEntreprise"/>
                    <field name="login" type="text" column="Login"/>
                    <field name="password" type="password" column="Password"/>
                    <field name="remarque" type="text" column="Remarque"/>
                </fields>
            </group>
        </groups>
        <views>
            <view name="viewDocuments" style="documents">
                <group name="info"/>
            </view>
        </views>
    </model>
</models>

On prévoit 3 niveaux d'utilisateurs:

  • contact : ce compte reçoit les rôles EWT et EWT-contact
  • responsable : ce compte reçoit les rôles EWT et EWT-responsable
  • admin : ce compte reçoit les rôles EWT et EWT-admin

L'idée est d'associer des contacts à des entreprises. Pour l'exercice, on prévoit les contraintes de sécurité suivantes:

  • Les contacts ont la possibilité de modifier les infos de leur compte personnel, à l'exception de la référence à l'entreprise et du champ "remarque". On souhaite que le champ remarque soit en lecture seule et que le champ "entreprise" ne soit pas visible de la personne.

  • Les responsables ont tous les droits sur la fiche d'entreprise à laquelle ils sont rattachés. Ils peuvent consulter les fiches des autres entreprises en lecture seule.

    Ils ont également la possibilité de consulter les fiches de contact. La fiche personnelle est consultable en lecture et écriture. Les fiches des autres contacts sont accessibles en consultation uniquement, à l'exception toutefois de la référence d'entreprise et de la remarque, qui sont modifiables sous certaines conditions:

    • La référence d'entreprise n'est modifiable que si elle est vide ou si elle correspond à l'entreprise à laquelle le responsable est lui-même rattaché.
    • Le champ remarque n'est modifiable que dans le second cas de figure évoqué ci-dessus.
  • Les administrateurs ont les droits sur tous les dossiers et tous les champs.

Note

Pour des raisons de clarté, notre exemple considère une application sans gestion d'états. De ce fait, les policies présentées ci-après ne contiennent aucune règle concernant les transitions.

Mise en œuvre

Il est possible d'aborder l'exercice de différentes façons. Pour commencer, nous utilisons une gestion de droits uniquement basée sur des policies d'identité.

Dans un second temps, nous effectuerons une approche au moyen de policies basées sur les ressources.

Approche au moyen de policies basées sur les identités

Policy de base

Une policy de base main.policy est pré-définie et donne accès à l'application en lecture seule à tout utilisateur qui possède le rôle EWT. Les utilisateurs qui ne possèdent pas ce rôle recevront une erreur HTTP 403.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="main" type="identity" priority="-100">
    <description>
        Cette policy d'identité vérifie le rôle technique permettant d'accéder
        à l'application. Comme il s'agit de la policy de base, on lui donne une
        priorité de -100, ce qu'il fait que n'importe quelle autre policy prime
        sur celle-ci.
    </description>
    <appliesTo>
        <role>EWT</role>
    </appliesTo>
    <statement effect="allow">
        <!-- la policy autorise l'accès uniquement en lecture -->
        <permission>access,read</permission>
        <!-- on autorise la sauvegarde et l'évaluation de script (pour
             la recherche -->
        <action>dummy,save,script</action>
    </statement>
</policy>
Administrateur

Le niveau administrateur est très simple à mettre en œuvre. La policy d'identité suivante donne accès à tout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="admin" type="identity" priority="0">
    <description>
        Policy d'identité relative à un administrateur
    </description>
    <appliesTo>
        <role>EWT-admin</role>
    </appliesTo>
    <statement effect="allow">
        <description>
            On autorise un accès complet sur toute l'application.
            On ne spécifie aucune ressource ici car on ne souhaite pas que le
            statement soit dépendant d'une ressource en particulier.
        </description>
        <permission>*</permission>
        <action>*</action>
    </statement>
</policy>
Contact

Pour rappel, les contacts ont la possibilité de modifier les infos de leur compte personnel, à l'exception de la référence à l'entreprise et du champ "remarque". On souhaite que le champ remarque soit en lecture seule et que le champ "entreprise" ne soit pas visible de la personne.

L'accès à la fiche personnelle se fait au moyen de la recherche. Une autorisation à évaluer le script de recherche est donc également nécessaire. La policy ci-dessous implémente ces différents aspects.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="contact" type="identity" priority="0">
    <description>
        Policy d'identité relative à une personne de contact.
        Les contacts ont la possibilité de modifier les infos de leur compte
        personnel, à l'exception de la référence à l'entreprise et du champ
        "remarque". Ils n'ont cependant aucune autre possibilité que l'édition.
    </description>
    <appliesTo>
        <role>EWT-contact</role>
    </appliesTo>

    <statement effect="allow">
        <description>
            Ce statement définit les droits de la personne de contact au sein
            de sa fiche personnelle.
        </description>
        <resource>personne</resource>
        <permission>*</permission>
        <action>save,open,close</action>
        <condition require="docId">
            // le test ci-dessous retourne true si le user connecté a le même
            // login que celui enregistré dans la fiche de personne
            return ($request.getPrincipal() == $str.lowercase(#login));
        </condition>
    </statement>

    <statement effect="deny">
        <description>
            Le statement précédent a donné les permissions read et write sur
            tous les champs du modèle "personne". Ici on va retirer le droit
            d'écriture sur les champs remarque et idEntreprise
        </description>
        <resource>personne.info.idEntreprise</resource>
        <resource>personne.info.remarque</resource>
        <permission>write</permission>
    </statement>

    <statement effect="deny">
        <description>
            On retire également le droit de lecture sur le champ idEntreprise
        </description>
        <resource>personne.info.idEntreprise</resource>
        <permission>read</permission>
    </statement>
</policy>
Responsable

Pour rappel, les responsables ont tous les droits sur la fiche d'entreprise à laquelle ils sont rattachés. Ils peuvent consulter les fiches des autres entreprises en lecture seule.
Les responsables ont également la possibilité de consulter les fiches de contact, toutefois seules la référence d'entreprise et la remarque sont modifiables sous certaines conditions:

  • La référence d'entreprise n'est modifiable que si elle est vide ou si elle correspond à l'entreprise à laquelle le responsable est lui-même rattaché.
  • Le champ remarque n'est modifiable que dans le second cas de figure évoqué ci-dessus.

Nous pouvons implémenter les droits du responsable sur sa fiche entreprise de deux manières:

  1. On peut définir un statement qui accorde les droits en lecture/écriture
    sur tous les dossiers "entreprise", et définir un second statement qui bloque l'écriture lorsque l'idEntreprise courant ne correspond pas à celui de l'utilisateur.o
  2. À l'inverse, on peut définir un statement qui autorise la lecture seule sur tous les dossiers "entreprise", et définir un second statement qui étend le droit à l'écriture lorsque l'idEntreprise correspond à celui de l'utilisateur.

Dans l'implémentation ci-dessous nous mettons en œuvre cette seconde option, pour deux raisons: d'une part cette manière de gérer les droits par ajout d'autorisation est plus propre (cela évite d'ajouter beaucoup de droits pour en retirer ensuite) et d'autre part cela permet de gérer également l'accès en lecture seule aux fiches de personnes via le même statement.

La policy d'identité pour les responsables peut être formulée ainsi:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="responsable" type="identity" priority="0">
    <description>
        Policy d'identité relative à un responsable d'entreprise.
        Les responsables ont tous les droits sur la fiche d'entreprise à
        laquelle ils sont rattachés (i.e. entreprise référencées via le
        champ idEntreprise de leur fiche personnelle).
        Ils ont la possibilité de consulter les fiches de personnes et de
        modifier les infos suivantes uniquement:
          - la référence d'entreprise ne peut être modifiée que si elle est
            vide ou si elle correspond à la référence qui figure dans la
            fiche personnelle du responsable
          - la remarque est modifiable uniquement si la référence d'entreprise
            de la fiche correspond à celle de la fiche personnelle du
            responsable.
    </description>
    <appliesTo>
        <role>EWT-responsable</role>
    </appliesTo>

    <statement effect="allow">
        <description>
            Ce statement accorde un accès en lecture à tout dossier des modèles
            entreprise et personne.
        </description>
        <resource>entreprise,personne</resource>
        <permission>read</permission>
        <action>open,save,close</action>
    </statement>

    <statement effect="allow">
        <description>
            Ce statement étend les droits du statement précédent et accorde un
            accès en écriture au dossier qui appartiennent au responsable, i.e.
            la fiche entreprise qui correspond à l'idEntreprise de la fiche
            personnelle de l'utilisateur et la fiche personnelle elle-même.
        </description>
        <resource>entreprise,personne</resource>
        <permission>write</permission>
        <action>delete,addTuple,delTuple</action>
        <condition require="docId">
            // on n'évalue cette condition que si le dossier est une entreprise
            // cela évite que le script essaie de charger une fiche entreprise
            // alors qu'on travaille sur une personne
            context "entreprise" {
              var monEntreprise = $sql.select(`select idEntreprise
                                               from Personne
                                               where lower(Login)=?`::T,
                                              [ $request.getPrincipal() ]);
              return (#entreprise.idEntreprise == monEntreprise);
            }
        </condition>
        <condition require="docId">
            // on limite l'évaluation au modèle personne
            context "personne" {
              return ($request.getPrincipal() == $str.lowercase(#personne.login));
            }
        </condition>
    </statement>

    <statement effect="allow">
        <description>
            Ce statement accorde le droit d'écriture sur le champ idEntreprise
            de la fiche personne lorsque l'entreprise en question est vide ou
            qu'elle correspond à l'entreprise du responsable
        </description>
        <resource>personne.info.idEntreprise</resource>
        <permission>write</permission>
        <condition>
            // #idEntreprise est une valeur typée, dans le cas présent : un
            // integer. Donc pour tester s'il est non renseigné, on doit
            // vérifier s'il vaut null; on aurait également pu faire le test
            // sous la forme "${data:idEntreprise}" == "" car dans ce cas on
            // aurait une représentation de la valeur sous forme de string.

            return #idEntreprise == null ||
                   $sql.select(`select idEntreprise
                                from Personne
                                where lower(Login)=?`::T,
                               [ $request.getPrincipal() ]) == #idEntreprise;
        </condition>
    </statement>

    <statement effect="allow">
        <description>
            Ce statement accorde le droit d'écriture sur le champ remarque de
            la fiche personne lorsque celle-ci référence explicitement
            l'entreprise du responsable
        </description>
        <resource>personne.info.remarque</resource>
        <permission>write</permission>
        <action>save</action>
        <condition>
            return $sql.select(`select idEntreprise
                                from Personne
                                where lower(Login)=?`::T,
                               [ $request.getPrincipal() ]) == #idEntreprise;
        </condition>
    </statement>
</policy>
Bilan

L'approche par policy basée sur les identités est fonctionnelle. Elle nécessite l'écriture de 8 statements.

On remarque toutefois qu'elle entraîne des répétitions au niveau des conditions des statements, en particulier au niveau de la policy "responsable".

Dans la pratique, on évitera la répétition de ces requêtes SQL. Pour ce faire, une solution serait d'utiliser un persistent object dans lequel on pourrait stocker l'identifiant de l'entreprise du responsable. Les objets persistents sont des objets stockés au niveau de la session. Ils sont donc disponibles en tout temps, tout au long de la session.

Une solution alternative est à l'étude dans Ewt (mais non implémentée actuellement). Il s'agirait de construire un rôle dérivé de type "owner" qui représente un propriétaire de ressource. La notion de rôle dérivé s'inspire de cerbos soient également repris dans Ewt, en particulier les rôles dérivés.

Approche au moyen de policies de ressource

Dans une approche basée sur des policies de ressource, il ne faut plus chercher à voir ce que peut faire un utilisateur, mais chercher à déterminer ce que les ressources autorisent. L'idée ici n'est plus d'attribuer des droits en fonction du type de sujet connecté, mais de décrire quels sujets ont le droit d'accéder aux ressources.

La première étape consiste donc à identifier les ressources. Dans notre exercice, nous avons:

  • l'application
  • le modèle "entreprise"
  • le modèle "personne"
  • le champ "personne.info.idEntreprise"
  • le champ "personne.info.remarque"

Nous allons donc définir une policy pour chacune de ces ressources.

Application

La policy "main" permet de définir les droits sur l'application. Dans le cas de notre exercice, la policy doit a minima accorder un droit en lecture seule sur la page d'accueil.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="main" type="resource" priority="-100">
    <description>
        Cette policy décrit les autorisations de base.
    </description>

    <statement effect="allow">
        <description>
            Ce statement accorde un droit de lecture sur l'application.
        </description>
        <subject>
            <role>EWT</role>
        </subject>
        <permission>access,read</permission>
        <action>dummy,save,script</action>
    </statement>

    <statement effect="allow">
        <description>
            Le rôle EWT-admin a tous les droits sur l'application. On définit
            donc cette autorisation également au niveau de la policy main car
            elle est valable pour toute l'application.
        </description>
        <subject>
            <role>EWT-admin</role>
        </subject>
        <permission>*</permission>
        <action>*</action>
    </statement>
</policy>

La policy ci-dessous doit être comprise ainsi: La policy main accorde une permission read et autorise les actions dummy, save et script aux utilisateurs qui ont un rôle EWT. Elle accorde en outre toutes les permissions et autorise toutes les actions au rôle EWT-admin.

Modèle "entreprise"

La fiche entreprise est accessible en lecture seule par les responsables. Elle est accessible en écriture par le responsable qui est rattaché à ladite fiche ainsi que par les administrateurs. On peut ignorer les administrateurs ici car les droits de ces derniers sont déjà entièrement gérés par la policy "main".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="entreprise" type="resource" priority="0">
    <description>
        Cette policy de ressource décrit les droits relatifs au modèle 
        "entreprise"
    </description>

    <statement effect="allow">
        <description>
            Ce statement accorde le droit de lecture sur les fiches entreprise 
            pour les responsables.
            Note : Il n'est pas nécessaire de référencer le rôle EWT-admin 
            ici car ce dernier est déjà géré au niveau de l'application via la
            policy main.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>read</permission>
        <action>open,save,close,create</action>
    </statement>

    <statement effect="allow"">
        <description>
            Ce statement accorde le droit d'écriture à un responsable sur la
            fiche entreprise s'il en est lui-même responsable.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>write</permission>
        <action>delete,addTuple,delTuple</action>
        <condition require="docId">
            return #idEntreprise == null ||
                   $sql.select(`select idEntreprise
                                from Personne
                                where lower(Login)=?`::T,
                               [ $request.getPrincipal() ]) == #idEntreprise;
        </condition>
    </statement>
</policy>

Il reste à faire le lien entre le modèle "entreprise" et la policy "entreprise". Pour cela, il suffit d'ajouter une référence au niveau de la descript:

1
2
3
4
5
6
7
8
9
<model name="entreprise" maingroup="info" mainfield="idEntreprise">
    <policies>
        <policy name="entreprise"/>
    </policies>
    <groups>
        ...
    </groups>
    ...
</model>

Une notation plus courte est également possible:

1
2
3
4
5
6
<model name="entreprise" maingroup="info" mainfield="idEntreprise" policy="entreprise">
    <groups>
        ...
    </groups>
    ...
</model>
Modèle "personne"

Les règles définies pour cet exercice sont implémentables au moyen de trois policies : l'une pour le modèle, l'une pour le champ "idEntreprise" et la dernière pour le champ "remarque". La descript du modèle ressemble donc à ceci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<model name="personne" maingroup="info" mainfield="idPersonne">
    <policies>
        <policy name="personne"/>
    </policies>
    <groups>
        <group name="info" table="Personne" type="single">
            <fields>
                <field name="idPersonne" type="hidden" column="idPersonne"/>
                <field name="nom" type="text" column="Nom"/>
                <field name="prenom" type="text" column="Prenom"/>
                <field name="email" type="email" column="EMail"/>
                <field name="idEntreprise" type="text" column="idEntreprise">
                    <policies>
                        <policy name="personne-idEntreprise"/>
                    </policies>
                </field>
                <field name="login" type="text" column="Login"/>
                <field name="password" type="password" column="Password"/>
                <field name="remarque" type="text" column="Remarque">
                    <policies>
                        <policy name="personne-remarque"/>
                    </policies>
                </field>
            </fields>
        </group>
    </groups>
    <views>
        <view name="viewDocuments" style="documents">
            <group name="info"/>
        </view>
    </views>
</model>

La policy qui gère le modèle est la suivante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="personne" type="resource" priority="0">
    <description>
        Cette policy de ressource décrit les droits relatifs au modèle 
        "personne"
    </description>

    <statement effect="allow">
        <description>
            Ce statement accorde un droit en lecture au contact sur son propre
            dossier. Le refus d'accès sur les autres fiches de personnes reste
            de vigueur (le deny est la règle par défaut sur Ewt).
        </description>
        <subject>
            <role>EWT-contact</role>
        </subject>
        <permission>read,write</permission>
        <action>open,save,close,delete,addTuple,delTuple</action>
        <condition require="docId">
            return $str.lowercase(#login) == $request.getPrincipal();
        </condition>
    </statement>

    <statement effect="allow">
        <description>
            Ce statement accorde un droit de lecture au responsable.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>read</permission>
        <action>open,save,close</action>
    </statement>

    <statement effect="allow">
        <description>
            Ce statement accorde tous les droits à un responsable sur la
            fiche personnelle.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>write</permission>
        <action>open,save,close,delete,addTuple,delTuple</action>
        <condition require="docId">
            return $str.lowercase(#login) == $request.getPrincipal();
        </condition>
    </statement>
</policy>

La policy qui gère les autorisations relatives au champ "idEntreprise" est:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="personne-idEntreprise" type="resource" priority="0">
    <description>
        Cette policy gère le cas particulier des droits du champ
        personne.info.idEntreprise.
    </description>

    <statement effect="deny">
        <description>
            Ce statement retire tous les droits sur le champ idEntreprise
            à un utilisateur de niveau contact.
        </description>
        <subject>
          <role>EWT-contact</role>
        </subject>
        <permission>read,write</permission>
    </statement>

    <statement effect="allow">
        <description>
            Le responsable est autorisé à modifier la référence d'entreprise
            si celle-ci est vide ou qu'elle référence l'entreprise du 
            responsable.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>read,write</permission>
        <condition require="docId">
            return #idEntreprise == null ||
                   $sql.select(`select idEntreprise
                                from Personne
                                where lower(Login)=?`::T,
                               [ $request.getPrincipal() ]) == #idEntreprise;
        </condition>
    </statement>
</policy>

La policy qui gère les autorisations relatives au champ "remarque" est:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="personne-remarque" type="resource" priority="0">
    <description>
        Cette policy gère le cas particulier des droits du champ
        personne.info.remarque.
    </description>

    <statement effect="deny">
        <description>
            Ce statement retire le droit d'écriture sur le champ remarque
            pour les utilisateurs ayant un niveau contact.
        </description>
        <subject>
            <role>EWT-contact</role>
        </subject>
        <permission>write</permission>
    </statement>

    <statement effect="allow">
        <description>
            Le responsable est autorisé à modifier la remarque sur la fiche
            d'une personne rattachée à l'entreprise du responsable.
        </description>
        <subject>
            <role>EWT-responsable</role>
        </subject>
        <permission>read,write</permission>
        <condition require="docId">
            return $sql.select(`select idEntreprise
                                from Personne
                                where lower(Login)=?`::T,
                               [ $request.getPrincipal() ]) == #idEntreprise;
        </condition>
    </statement>
</policy>
Bilan

L'approche par policy basée sur les ressources est également fonctionnelle et le résultat final est équivalent si l'on se base sur le fonctionnement de l'application. Elle fait intervenir 11 statements. C'est plus que pour l'approche utilisant des policies basées sur les identités, mais ce n'est pas forcément un signe de moins bonne performance : dans le cas des policies de ressources, le moteur n'est pas obligé d'évaluer toutes les policies. Il n'évalue que celles qui apportent une information utile. En effet, le moteur n'a pas besoin d'évaluer les policies référencées dans le modèle "personne" lorsqu'il traite un dossier du modèle "entreprise".

Des deux approches, il est difficile de dire laquelle est la plus simple et laquelle est la plus performante. Dans cet exemple nous avons confronté deux approches diamétralement opposées. Toutefois il est possible de mélanger les différents types de policies : certains aspects peuvent être traités au moyen de policies basées sur les identités alors que d'autres peuvent s'appuyer sur des policies de ressources.

Approche basée sur la hiérarchie d'accès

Dans les deux approches ci-dessus, nous n'avons pas tenu compte de la hiérarchie qui peut être établie entre les différents rôles. Une solution alternative pourrait être d'implémenter les policies sous forme de niveaux d'accès ou de niveaux de privilèges. Le rôle "contact" aurait un niveau de base, le rôle "responsable" un niveau intermédiaire et le rôle "admin" le niveau maximum.

On peut alors construire des policies pour chaque niveau de privilège de 0 à 9. Pour le niveau 6, cela pourrait ressembler à:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8" ?>
<policy name="priv-6" type="resource">
    <description>
        Cette policy autorise un accès complet à tous roles 6 et plus.
    </description>

    <statement effect="allow">
        <subject>
            <role>EWT-6</role>
            <role>EWT-7</role>
            <role>EWT-8</role>
            <role>EWT-9</role>
        </subject>
        <permission>*</permission>
        <action>*</action>
    </statement>

    <statement effect="deny">
        <subject>
            <role>EWT-0</role>
            <role>EWT-1</role>
            <role>EWT-2</role>
            <role>EWT-3</role>
            <role>EWT-4</role>
            <role>EWT-5</role>
        </subject>
        <permission>*</permission>
        <action>*</action>
    </statement>
</policy>

Il est possible de définir des policies équivalentes pour les rôles numérotés de 0 à 9. Ensuite il suffit de référencer les policies au niveau de la descript. Cela permet d'implémenter une gestion de droits basée sur les niveaux de privilèges.

Attention toutefois, la policy ci-dessus ne fait pas de distinction entre l'accès en lecture et l'accès en écriture. Il faudrait encore définie des policies spécifiques pour les différents types d'accès.