Publié

~8mn de lecture 🕑

mar. 12 mai 2015

← Liste des articles

La gestion des permissions

Read the English version

Dans le cadre de la création d'un service de stockage de données personnelles (Kinto), la gestion des permissions est un des gros challenges : qui doit avoir accès à quoi, et comment le définir ?

tl;dr: Quelques retours sur le vocabulaire des systèmes de permission et sur nos idées pour l'implementation des permissions dans un stockage générique.

La problématique

La problématique est simple : des données sont stockées en ligne, et il faut un moyen de pouvoir les partager avec d'autres personnes.

En regardant les cas d'utilisations, on se rend compte qu'on a plusieurs types d'utilisateurs :

  • les utilisateurs "finaux" (vous) ;
  • les applications qui interagissent en leurs noms.

Tous les intervenants n'ont donc pas les mêmes droits : certains doivent pouvoir lire, d'autres écrire, d'autres encore créer de nouveaux enregistrements, et le contrôle doit pouvoir s'effectuer de manière fine : il doit être possible de lire un enregistrement mais pas un autre, par exemple.

Nous sommes partis du constat que les solutions disponibles n'apportaient pas une réponse satisfaisante à ces besoins.

Un problème de vocabulaire

Le principal problème rencontré lors des réflexions fût le vocabulaire.

Voici ci-dessous une explication des différents termes.

Le concept de « principal »

Un principal, en sécurité informatique, est une entité qui peut être authentifiée par un système informatique. [1] En Français il s'agit du « commettant », l'acteur qui commet l'action (oui, le terme est conceptuel !)

Il peut s'agir aussi bien d'un individu, d'un ordinateur, d'un service ou d'un groupe regroupant l'une de ces entités, ce qui est plus large que le classique « user id ».

Les permissions sont alors associées à ces principals.

Par exemple, un utilisateur est identifié de manière unique lors de la connexion par le système d'authentification dont le rôle est de définir une liste de principals pour l'utilisateur se connectant.

[1]Pour en savoir plus sur les principals : https://en.wikipedia.org/wiki/Principal_%28computer_security%29

La différence entre rôle et groupe

De but en blanc, il n'est pas évident de définir précisément la différence entre ces deux concepts qui permettent d'associer des permissions à un groupe de principals. [2]

La différence est principalement sémantique. Mais on peut y voir une différence dans la « direction » de la relation entre les deux concepts.

  • Un rôle est une liste de permissions que l'on associe à un principal.
  • Un groupe est une liste de principals que l'on peut associer à une permission.
[2]Plus d'informations : http://stackoverflow.com/questions/7770728/group-vs-role-any-real-difference

La différence entre permission, ACL, ACE

Une ACL est une liste d’Access Control Entry (ACE) ou entrée de contrôle d'accès donnant ou supprimant des droits d'accès à une personne ou un groupe.

https://fr.wikipedia.org/wiki/Access_Control_List

Je dirais même plus, dans notre cas, « à un principal ». Par exemple:

create_record: alexis,remy,tarek

Cet ACE donne la permission create sur l'objet record aux utilisateurs Tarek, Rémy et Alexis.

La délégation de permissions

Imaginez l'exemple suivant, où un utilisateur stocke deux types de données en ligne :

  • des contacts ;
  • une liste de tâches à faire qu'il peut associer à ses contacts.

L'utilisateur a tous les droits sur ses données.

Cependant il utilise deux applications qui doivent elles avoir un accès restreint :

  • une application de gestion des contacts à qui il souhaite déléguer la gestion intégrale de ses contacts : contacts:write ;
  • une application de gestion des tâches à qui il souhaite déléguer la gestion des tâches : contacts:read tasks:write

Il souhaite que son application de contacts ne puisse pas accéder à ses tâches et que son application de tâches ne puisse pas modifier ses contacts existants, juste éventuellement en créer de nouveaux.

Il lui faut donc un moyen de déléguer certains de ses droits à un tiers (l'application).

C'est précisément le rôle des scopes OAuth2.

Lors de la connexion d'un utilisateur, une fenêtre lui demande quels accès il veut donner à l'application qui va agir en son nom.

Le service aura ensuite accès à ces scopes en regardant le jeton d'authentification utilisé. Cette information doit être considérée comme une entrée utilisateur (c'est à dire qu'on ne peut pas lui faire confiance). Il s'agit de ce que l'utilisateur souhaite.

Or, il est également possible que l'utilisateur n'ait pas accès aux données qu'il demande. Le service doit donc utiliser deux niveaux de permissions : celles de l'utilisateur, et celles qui ont été déléguées à l'application.

Espace de noms

Dans notre implémentation initiale de Kinto (notre service de stockage en construction), l'espace de nom était implicite : les données stockées étaient nécessairement celles de l'utilisateur connecté.

Les données d'un utilisateur étaient donc cloisonnées et impossible à partager.

L'utilisation d'espaces de noms est une manière simple de gérer le partage des données.

Nous avons choisi de reprendre le modèle de GitHub et de Bitbucket, qui utilisent les « organisations » comme espaces de noms.

Dans notre cas, il est possible de créer des "buckets", qui correspondent à ces espaces de noms. Un bucket est un conteneur de collections et de groupes utilisateurs.

Les ACLs sur ces collections peuvent être attribuées à certains groupes du bucket ainsi qu'à d'autres principals directement.

Notre proposition d'API

Les objets manipulés

Pour mettre en place la gestion des permissions, nous avons identifié les objets suivants :

Objet Description
bucket On peut les voir comme des espaces de noms. Ils permettent d'avoir différentes collections portant le même nom mais stockées dans différents buckets de manière à ce que les données soient distinctes.
collection Une liste d'enregistrements.
record Un enregistrement d'une collection.
group Un groupe de commetants (« principals »).

Pour la définition des ACLs, il y a une hiérarchie et les objets « héritent » des ACLs de leur parents :

           +---------------+
           | Bucket        |
           +---------------+
    +----->+ - id          +<---+
    |      | - permissions |    |
    |      +---------------+    |
    |                           |
    |                           |
    |                           |
    |                           |
    |                           |
+---+-----------+        +------+---------+
| Collection    |        | Group          |
+---------------+        +----------------+
| - id          |        |  - id          |
| - permissions |        |  - members     |
+------+--------+        |  - permissions |
       ^                 +----------------+
       |
       |
+------+---------+
| Record         |
+----------------+
|  - id          |
|  - data        |
|  - permissions |
+----------------+

Les permissions

Pour chacun de ces objets nous avons identifié les permissions suivantes :

Permission Description
read La permission de lire le contenu de l'objet et de ses sous-objets.
write La permission de modifier et d'administrer un objet et ses sous- objets. La permission write permet la lecture, modification et suppression d'un objet ainsi que la gestion de ses permissions.
create La permission de créer le sous-objet spécifié. Par exemple: collections:create

À chaque permission spécifiée sur un objet est associée une liste de principals.

Dans le cas de la permission create on est obligé de spécifier l'objet enfant en question car un objet peut avoir plusieurs types d'enfants. Par exemple : collections:create, groups:create.

Nous n'avons pour l'instant pas de permission pour delete et update, puisque nous n'avons pas trouvé de cas d'utilisation qui les nécessitent. Quiconque avec le droit d'écriture peut donc supprimer un enregistrement.

Les permissions d'un objet sont héritées de son parent. Par exemple, un enregistrement créé dans une collection accessible à tout le monde en lecture sera lui aussi accessible à tout le monde.

Par conséquent, les permissions sont cumulées. Autrement dit, il n'est pas possible qu'un objet ait des permissions plus restrictives que son parent.

Voici la liste exhaustive des permissions :

Objet Permissions associées Commentaire
Configuration (.ini) buckets:create Les principals ayant le droit de créer un bucket sont définis dans la configuration du serveur. (ex. utilisateurs authentifiés)
bucket write C'est en quelque sorte le droit d'administration du bucket.
read C'est le droit de lire le contenu de tous les objets du bucket.
collections:create Permission de créer des collections dans le bucket.
groups:create Permission de créer des groupes dans le bucket.
collection write Permission d'administrer tous les objets de la collection.
read Permission de consulter tous les objets de la collection.
records:create Permission de créer des nouveaux enregistrement dans la collection.
record write Permission de modifier ou de partager l'enregistrement.
read Permission de consulter l'enregistrement.
group write Permission d'administrer le groupe
read Permission de consulter les membres du groupe.

Les « principals »

Les acteurs se connectant au service de stockage peuvent s'authentifier.

Ils reçoivent alors une liste de principals :

  • Everyone: le principal donné à tous les acteurs (authentifiés ou pas) ;
  • Authenticated: le principal donné à tous les acteurs authentifiés ;
  • un principal identifiant l'acteur, par exemple fxa:32aa95a474c984d41d395e2d0b614aa2

Afin d'éviter les collisions d'identifiants, le principal de l'acteur dépend de son type d'authentification (system, basic, ipaddr, hawk, fxa) et de son identifiant (unique par acteur).

En fonction du bucket sur lequel se passe l'action, les groupes dont fait partie l'utilisateur sont également ajoutés à sa liste de principals. group:moderators par exemple.

Ainsi, si Bob se connecte avec Firefox Accounts sur le bucket servicedenuages_blog dans lequel il fait partie du groupe moderators, il aura la liste de principals suivante : Everyone, Authenticated, fxa:32aa95a474c984d41d395e2d0b614aa2, group:moderators

Il est donc possible d'assigner une permission à Bob en utilisant l'un de ces quatre principals.

Note

Le principal <userid> dépend du back-end d'authentification (e.g. github:leplatrem).

Quelques exemples

Blog

Objet Permissions Principals
bucket:blog write fxa:<blog owner id>
collection:articles write group:moderators
read Everyone
record:569e28r98889 write fxa:<co-author id>

Wiki

Object Permissions Principals
bucket:wiki write fxa:<wiki administrator id>
collection:articles write Authenticated
read Everyone

Sondages

Objet Permissions Principals
bucket:poll write fxa:<admin id>
collection:create Authenticated
collection:<poll id> write fxa:<poll author id>
record:create Everyone

Cartes colaboratives

Objet Permissions Principals
bucket:maps write fxa:<admin id>
collection:create Authenticated
collection:<map id> write fxa:<map author id>
read Everyone
record:<record id> write fxa:<maintainer id> (ex. event staff member maintaining venues)

Plateformes

Bien sûr, il y a plusieurs façons de modéliser les cas d'utilisation typiques. Par exemple, on peut imaginer une plateforme de wikis (à la wikia.com), où les wikis sont privés par défaut et certaines pages peuvent être rendues publiques :

Objet Permissions Principals
bucket:freewiki write fxa:<administrator id>
collection:create Authenticated
group:create Authenticated
collection:<wiki id> write fxa:<wiki owner id>, group:<editors id>
read group:<readers id>
record:<page id> read Everyone

L'API HTTP

Lors de la création d'un objet, l'utilisateur se voit attribué la permission write sur l'objet :

PUT /v1/buckets/servicedenuages_blog HTTP/1.1
Authorization: Bearer 0b9c2625dc21ef05f6ad4ddf47c5f203837aa32ca42fced54c2625dc21efac32
Accept: application/json

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
    "id": "servicedenuages_blog",
    "permissions": {
        "write": ["fxa:49d02d55ad10973b7b9d0dc9eba7fdf0"]
    }
}

Il est possible d'ajouter des permissions à l'aide de PATCH :

PATCH /v1/buckets/servicedenuages_blog/collections/articles HTTP/1.1
Authorization: Bearer 0b9c2625dc21ef05f6ad4ddf47c5f203837aa32ca42fced54c2625dc21efac32
Accept: application/json

{
    "permissions": {
        "read": ["+system.Everyone"]
    }
}

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{
    "id": "servicedenuages_blog",
    "permissions": {
        "write": ["fxa:49d02d55ad10973b7b9d0dc9eba7fdf0"],
        "read": ["system.Everyone"]
    }
}

Pour le PATCH nous utilisons la syntaxe préfixée par un + ou par un - pour ajouter ou enlever des principals sur un ACL.

Il est également possible de faire un PUT pour réinitialiser les ACLs, sachant que le PUT va ajouter l'utilisateur courant à la liste automatiquement mais qu'il pourra se retirer avec un PATCH. Ajouter l'utilisateur courant permet d'éviter les situations où plus personne n'a accès aux données.

Note

La permission create est valable pour POST mais aussi pour PUT lorsque l'enregistrement n'existe pas.

Le cas spécifique des données utilisateurs

Une des fonctionnalités actuelles de Kinto est de pouvoir gérer des collections d'enregistrements par utilisateur.

Sous *nix il est possible, pour une application, de sauvegarder la configuration de l'utilisateur courant dans son dossier personnel sans se soucier de l'emplacement sur le disque en utilisant ~/.

Dans notre cas si une application souhaite sauvegarder les contacts d'un utilisateur, elle peut utiliser le raccourci ~ pour faire référence au bucket personnel de l'utilisateur : /buckets/~/collections/contacts

Cette URL retournera le code HTTP 307 vers le bucket de l'utilisateur courant :

POST /v1/buckets/~/collections/contacts/records HTTP/1.1

{
   "name": "Rémy",
   "emails": ["remy@example.com"],
   "phones": ["+330820800800"]
}

HTTP/1.1 307 Temporary Redirect
Location: /v1/buckets/fxa:49d02d55ad10973b7b9d0dc9eba7fdf0/collections/contacts/records

Ainsi il est tout à fait possible à Alice de partager ses contacts avec Bob. Il lui suffit pour cela de donner la permission read à Bob sur sa collection et de donner l'URL complète /v1/buckets/fxa:49d02d55ad10973b7b9d0dc9eba7fdf0/collections/contacts/records à Bob.

La délégation des permissions

Dans le cas de Kinto, nous avons défini un format pour restreindre les permissions via les scopes OAuth2: storage:<bucket_id>:<collection_id>:<permissions_list>.

Ainsi, si on reprend l'exemple précédent de la liste de tâches, il est possible pour Bob de créer un token OAuth spécifique avec les scopes suivants : profile storage:todolist:tasks:write storage:~:contacts:read+records:create

Donc, bien que Bob a la permission write sur ses contacts, l'application utilisant ce token pourra uniquement lire les contacts existants et en ajouter de nouveaux.

Une partie de la complexité est donc de réussir à présenter ces scopes de manière lisible à l'utilisateur, afin qu'il choisisse quelles permissions donner aux applications qui agissent en son nom.

Voilà où nous en sommes de notre réflexion !

Si vous avez des choses à ajouter, des points de désaccord ou autres réflexions, n'hésitez pas à nous interrompre pendant qu'il est encore temps !

Revenir au début