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 :
| http://api.zestedesavoir.com/...
|
Pour la preprod, nous pouvons imaginer une adresse plus ou moins équivalente :
| 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 :
| 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 :
| {
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).
| {
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 :
| {
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 :
- 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.
- 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.
- 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.
- 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 :
| {
errors: [
{
code:4001,
message:"le paramètre indiqué pour la pagination n'est pas un nombre entier valide"
}
]
}
|
| {
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 :
| 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 :
| {
"access_token": "<your-access-token>",
"scope": "read",
"expires_in": 86399,
"refresh_token": "<your-refresh-token>"
}
|
- L'utilisateur se log pour la première fois grâce à un client à l'API
- Le serveur lui renvoi son access token avec son expiration et son refresh token sans expiration
- Durant la session, l'access token est utilisé dans les requêtes
- 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 :
| {
"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 |