ZEP-02 : Elaboration d'une API

a marqué ce sujet comme résolu.

Suite au développement de la ZEP 17 consacré aux membres, je vous propose cette nouvelle révision de cette ZEP mère pour y intégrer toutes les informations générales et ainsi pouvoir créer des nouvelles ZEP API sans les spécifier :

Cartouche
ZEP 2
Titre Elaboration d'une API
Révision 4
Date de création 11 Juillet 2014
Dernière révision 13 Janvier 2015
Type Feature
Statut Rédaction

Le contexte de la proposition

Il est presque inutile d'expliquer pourquoi cette fonctionnalité pourrait beaucoup apporter au projet. Selon moi, une API est totalement dans l'esprit de Zeste de Savoir en s'ouvrant au "monde" et donc, permettre à des projets tiers de l'utiliser (citons le projet Extensions Notificateurs) pour faciliter leur développement (pas cool de parser une page HTML et être dépendant de la structure d'une page qui peut évoluer à tout moment).

Objet de la proposition

Pour ceux qui vivent dans une grotte depuis plusieurs années, une API est une interface de programmation, une façade par laquelle un logiciel offre des services à d'autres logiciels. Par exemple, pour Zeste de Savoir, cela permettra de proposer, dans un format de données textuel comme le JSON, de fournir la liste des tutoriels selon certains filtres ou un tutoriel en particulier avec plus de détails.

L'API serait de type REST, donc orienté ressource. Chaque ressource est soumis à plusieurs méthodes HTTP du type : GET, POST, PUT et DELETE.

Informations sur une requête HTTP

Version de l'API

L'api est spécifié comme sous-domaine de l'URL pour ne pas la surcharger :

1
http://api.zestedesavoir.com/...

Pour la preprod, nous pouvons imaginer une adresse plus ou moins équivalente :

1
http://api.preprod.zestedesavoir.com/...
User-Agent

L'en-tête HTTP User-Agent doit être renseigné par le client.

Pour des informations complémentaires sur les User-Agent, rendez-vous sur son article Wikipedia.

Cross-Origin Request Sharing

Le serveur doit renvoyer les en-tête HTTP suivants afin d'éviter les soucis de CORS :

1
2
3
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link
Access-Control-Allow-Credentials: true

Pour des informations complémentaires sur CORS, rendez-vous sur son article Wikipedia version anglaise.

Considération générales

Gestion des droits d'accès aux ressources

La gestion des droits d'accès aux ressources n'a pas été statutée de manière définitive et restera du ressort du développeur de choisir la solution la plus adaptée en fonction du code existant dans le projet.

Ci-dessous les deux possibilités détaillées.

Possibilité n°1 : Plusieurs paths

La première solution consiste à faire plusieurs APIs (plusieurs paths REST) reposant sur un même socle côté serveur.

Avantages :

  • Plus simple de repérer les appels à l'API pour différents groupes
  • Ouvrir et fermer certaines routes directement dans un fichier de routes plutôt que dans des conditions (if/else).
  • Permet de mapper de façon plus naturelle sur le code serveur, plutôt que de router en fonction du token passé dans les headers HTTP.

Inconvénients :

  • Demande de rester cohérent entre tous les paths et de donner des responsabilités bien précises.
Possibilité n°2 : Un seul path et router sur le token

La seconde solution consiste à coller tout dans la même API et router en fonction du token passé dans les headers HTTP.

Avantages :

  • Gère un seul path par ressource. Les routes sont donc plus épurées.
  • Se rapproche sans doute plus du code actuel dans le back-end.

Inconvénients :

  • Pour un membre du groupe "staff" (par exemple), il ne peut pas "voir" des ressources comme un simple membre.
Formatage des erreurs

Lorsqu'un appel API provoque une erreur, il faut s'appuyer sur les codes HTTP standards. La réponse HTTP doit donc systématiquement utiliser un code interprétable pour le client. En plus de cela, le contenu de la réponse doit indiquer un code (par défaut le même que celui de la réponse HTTP, mais peut être différents pour donner de plus amples informations) ainsi qu'un message d'erreur compréhensible par le développeur du client de l'API.

Exemple, une réponse HTTP 404 devrait avoir le body suivant :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:404, 
            message:"Ce membre n'existe pas"
        }
    ]
}
Format d'entrée
  • A minima, le JSON doit être géré. Les autres formats sont facultatifs.
  • Lecture du header Content-Type, seule la valeur application/json est acceptée. Les autres formats donnent lieu à un code d'erreur HTTP 406 Not Acceptable. (NB : Twitter fait la différence entre le format des données envoyées et le format dans lequel les données sont envoyées. Dans le cas présent, ils renverraient une 404. A nous de décider ce qu'on fait).
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit envoyer des données au format application/json et positionner le header HTTP Content-Type de façon à le signaler"
        }
    ]
}
Format de sortie
  • A minima, le JSON doit être géré. Les autres formats sont facultatifs.
  • Lecture du header HTTP Accept, si la valeur application/json est absente, envoi d'un code HTTP 406 et de l'erreur suivante :
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:406, 
            message:"Le client doit accepter des réponses au format JSON et le signaler en positionnant le header HTTP Accept avec la valeur application/json"
        }
    ]
}
Format de sortie pour les fichiers de contenu

Les ressources Markdown sont renvoyées dans le corps de la requête HTTP et suivant le format indiqué dans le header de la requête avec la clé X-Data-Format.

Les formats supportés seront markdown, html et texte.

Caching

Pour obtenir un premier système de cache, l'ETag sera utilisé. L'avantage se situe principalement sur un niveau : d'alléger le back-office dans sa charge de travail. Il ne sera pas toujours nécessaire d'aller chercher l'information s'il remarque qu'elle n'a pas été modifiée entre deux appels.

Pour y parvenir, voici le fonctionnement basique d'un ETag :

  1. Lorsqu'un client fait appel à l'API, la réponse inclue un ETag avec une valeur correspondant à un hash de la donnée retournée par l'API. La valeur de cette ETag est sauvegardée pour une prochaine utilisation si nécessaire.
  2. Au prochain même appel du client sur l'API, le client peut inclure dans le header de sa requête, l'attribut If-None-Match avec le précédent ETag renvoyé par le back-office.
  3. Si l'ETag n'a pas changé, le code de la réponse sera 304 - Not Modified et aucune donnée ne sera renvoyée.
  4. Si l'ETAg a changé, le back-office fera la requête et renverra la donnée avec un nouveau ETag. Ce nouveau ETag sera sauvegardé et utilisé pour de prochains appels.
Pagination
  • Les résultats de requête doivent être paginés. Par défaut, une page de résultat doit contenir 10 résultats

  • L'utilisateur indique la page de résultats qu'il souhaite récupérer à l'aide du paramètre &page=. Dans le cas où ce paramètre est invalide (n'est pas un nombre entier ou en dehors des limites de la pagination), l'API retourne une erreur 4001 ou 4002 :

1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4001, 
            message:"le paramètre indiqué pour la pagination n'est pas un nombre entier valide"
        }
    ]
}
1
2
3
4
5
6
7
8
{
    errors: [
        {
            code:4002,
            message:"la page indiquée n'existe pas"
        }
    ]
}
  • L'API fournit des indications dans les headers de la réponse HTTP afin d'aider le client à naviguer dans les ressources paginées à l'aide du header Link. de la façon suivante :
1
2
3
4
Link: <http://zestedesavoir.com/membres/?search=toto&page=4>; rel="next",
  <http://zestedesavoir.com/membres/?search=toto&page=5>; rel="last",
  <http://zestedesavoir.com/membres/?search=toto&page=1>; rel="first",
  <http://zestedesavoir.com/membres/?search=toto&page=2>; rel="prev"

NB : Ne pas oublier de remettre tous les paramètres utilisés par le client dans l'URL (ici search=)

  • Dans de futures versions, l'utilisateur pourra indiquer le nombre de résultats souhaités en positionnant le paramètre &page_size= dans sa requête
Format de dates

Le format de dates retenu pour la communication avec le serveur est le format ISO 8601 (attention Java 7 nécessaire pour les utilisateurs de Java), pas de soucis en Python a priori.

En ce qui concerne les en-têtes HTTP, voici la liste des formats à gérer côté serveur.

Authentification

Permettre une authentification OAuth2 (dont la spécification du protocole est disponible via ce lien).

Son fonctionnement est le suivant :

  • Chaque client de l'API s'inscrit en tant qu'application tierce : ça signifie qu'il faut prévoir une table de stockage des clients de l'API
  • A la fin de l'inscription, le système devra renvoyer une ZDS_AUTH_KEY et une ZDS_AUTH_SECRET au développeur
  • Lorsqu'un membre veut se logguer au site via un client externe, le client devra donc envoyer à l'API:
    • le login : login de l'utilisateur sur le site
    • le password : mot de passe de l'utilisateur sur le site
    • le ZDS_AUTH_KEY : identifiant du client de l'API
    • le ZDS_AUTH_SECRET : clé secrète du client de l'API
  • Un token doit être généré qui signifie "le membre x veut se connecter à l'API via le client y". Ce token doit avoir une date d'expiration (à définir) et devra être utilisé par le client de l'API dans les entête HTTP.
  • A partir du token notamment on saura à quoi peut accéder tel ou tel membre.

L'authentification se base sur l'utilisation d'access token et de refresh token :

1
2
3
4
5
6
{
  "access_token": "<your-access-token>", 
  "scope": "read", 
  "expires_in": 86399, 
  "refresh_token": "<your-refresh-token>"
}
  1. L'utilisateur se log pour la première fois grâce à un client à l'API
  2. Le serveur lui renvoi son access token avec son expiration et son refresh token sans expiration
  3. Durant la session, l'access token est utilisé dans les requêtes
  4. A chaque fois que l'access token expire, le client refait une authentification avec le refresh token et la clé secrète

Ce processus a plusieurs intérêts :

  • Le token d'accès est le maillon faible, s'il est intercepté, tu peux prendre la place de l'utilisateur. En limitant sa durée de vie à une "session d'utilisation" (en gros inactivité pendant quelques minutes => expiration) voire en limitant sa durée de vie tout court à quelques minutes, tu limites le danger.

  • L'application n'a pas besoin de stocker le login et le mot de passe de l'utilisateur, et ne devrait pas le faire . Et c'est bien mieux comme ça pour tout le monde. (si jamais l'application est compromise, faille de sécurité, …)

  • Si la réponse du serveur contenant l'accessToken et le refreshToken est interceptée, l'accessToken va pouvoir être utilisé (mais c'est limité dans le temps… même si c'est extrêmement dangereux) par contre pour le refreshToken il faut le client_secret. Qui n'est pas dans la réponse du serveur.

  • Permet également à un client, n'importe où, n'importe quand d'invalider des accessToken, sans avoir besoin de saisir à nouveau ses login / mot de passe sur tous ses appareils. En gros, je dis "oulà, y'a une activité suspecte sur mon compte, un de mes accessToken a été intercepté, invalide les tous"

Route utilisée pour l'authentification :

URL : http://api.zestedesavoir.com/oauth2/access_token

Methode : POST

Headers : Aucun obligatoire

Paramètres :

  • client_id : Identifiant récupérer à la création du client.
  • client_secret : Secret récupérer à la création du client.
  • username : Username de l'utilisateur.
  • password : Mot de passe de l'utilisateur.
  • grant_type : password

Codes HTTP :

  • 200 : La réponse est renvoyée sans problème.

Permissions nécessaires : N/A

Exemple de résultat :

1
2
3
4
5
6
7
{
    "access_token": "27a59cd8b1f3322707781c7347ca20f8e141e594",
    "token_type": "Bearer",
    "expires_in": 31535999,
    "refresh_token": "e751cc50ae9a74e20d9d1f4890b86ab01ad5f343",
    "scope": "read"
}

Les moyens mis en œuvre

Comment nous pouvons développer cette API ? Il existe une librairie Django REST framework. Devant le fait que la plupart des méthodes d'une API se ressemble, il simplifie grandement sa réalisation pour les opérations usuelles CRUD.

L'approche pour la réalisation, au sens large, de l'API est la suivante : couvrir module par module, tel que les membres ou les forums, mais en suivant un workflow complet en lecture/écriture avec une ZEP pour chacun de ces modules. Cette solution offre des avantages, notamment :

  • Moins de développement est nécessaire pour une V1 comparé au temps de développement nécessaire pour couvrir tous les modules en lecture seule ;
  • Il pourrait y avoir plus de possibilités et d'utilité à sa sortie jusqu'à la V2 qui couvrirait d'autres modules ;
  • Cela assure qu'un workflow complet peut être couvert ; à savoir l'authentification, la consultation, l'écriture et la déconnexion. L'approche serait "verticale" ;
  • Les contributions se feront uniquement sur le module le plus stable du projet puisque les tutoriels et les articles ont pour vocation d'évoluer à court/moyen terme.

Différentes ZEP de l'API

ZEP Module concerné
ZEP-17 Membres
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte