Cartouche | |
---|---|
ZEP | 17 |
Titre | Elaboration de l'API des membres |
Révision | 5 |
Date de création | 3 octobre 2014 |
Dernière révision | 29 novembre 2014 |
Type | Feature |
Statut | Acceptée |
Contexte
Suite à des longues et constructives discussions sur la ZEP-02, qui concerne l'élaboration d'une API au sens large, il a été mentionné que son objectif, à terme, était trop ambitieux pour une seule ZEP. Afin d'alléger la ZEP
- En terme de contenu ;
- Pour ne pas débattre d'absolument toutes les problématiques rencontrées pour toutes les ressources proposées par Zeste de Savoir ;
- Et donc pour permettre à cette ZEP 17 de rediriger vers d'autres ZEP pour débattre de ses problématiques spécifiques à chaque ressource.
Cette ZEP 17 voit le jour pour finir la rédaction de l'API des membres.
Objet de la proposition
Réaliser l'API des membres comme première ressource permettra de :
- 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.
L'objectif concret de cette ZEP sera donc de développer complètement l'API des membres de bout en bout en y faisant les choix pour les autres API futures des autres ressources.
API des membres
Informations sur la requête HTTP
Version de l'API
1 | http://api.zestedesavoir.com/v0.0/... |
- La version est indiquée dans l'URL d'appel de l'API.
- L'api est spécifié comme sous-domaine de l'URL pour ne pas la surcharger.
Pour la prod, nous pouvons imaginer une adresse plus ou moins équivalente :
1 | http://api.preprod.zestedesavoir.com/v0.0/... |
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 valeurapplication/json
est acceptée. Les autres formats donnent lieu à un code d'erreur HTTP 406Not 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 valeurapplication/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 :
- 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 50 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
&perPage=
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.
Les différentes versions de l'API
Objectif
Fournir aux clients un ensemble d'outils pour consulter la liste des membres et leurs profils.
Représentation d'un membre
Exemple d'objet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { "id": 123456, "username": "Clem", "show_email": true, "email": "clem@zestedesavoir.com", "is_active": true, "site": "www.zestedesavoir.com", "avatar_url": "http://i.imgur.com/k6R1plZ.png", "biography": "Quae dum ita struuntur, indicatum est apud Tyrum indumentum regale textum occulte, incertum quo locante vel cuius usibus apparatum. ideoque rector provinciae tunc pater Apollinaris eiusdem nominis ut conscius ductus est aliique congregati sunt ex diversis civitatibus multi, qui atrocium criminum ponderibus urgebantur.", "sign": "", "email_for_answer": true, "last_visit": "1977-04-22T06:00:00Z", "date_joined": "1977-04-22T06:00:00Z" } |
J'ai délibérément omis les champs liés à la modération et spécifique au site comme le survol ou le clique pour le menu.
Colonnes
Colonne | Type | Description |
---|---|---|
id | long | Identifiant de l'utilisateur |
username | String | Nom d'utilisateur |
show_email | boolean | Affiche ou non l'e-mail. Si faux, le champ email ne sera pas spécifié dans l'objet |
String | E-mail de l'utilisateur | |
is_staff | boolean | Est un membre du staff ou non |
is_active | boolean | Est un compte actif ou non |
site | String | Site web de l'utilisateur |
avatar_url | String | Url vers l'avater de l'utilisateur |
biography | String | Biographie de l'utilisateur |
sign | String | Signature de l'utilisateur |
email_for_answer | boolean | Permet l'envoie d'e-mail depuis ZdS |
last_visit | Date | Dernière visite de l'utilisateur sur ZdS |
v0.0 : Afficher un profil utilisateur
Objectif
Mise en place simple du framework et routage sur une URL
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
GET | /v0.0/membres/{id} | Aucun | Renvoie la fiche profil d'un membre | 400 si l'identifiant fourni ne correspond pas au format d'identifiant attendu. 404 si le membre n'existe pas |
v0.1 : Lister les membres
Objectif
Renvoyer une liste au lieu d'un objet.
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
GET | /v0.1/membres/ | Aucun pour le moment, search pour de futures versions | Renvoie la liste des membres sans restriction (pagination ou différentielle) | Aucun autre que les codes standard |
v0.2 : Adaptation/ponts
Objectif
Définition de notre propre Serializer
: On renvoie un modèle différent du modèle stocké en base.
v0.3 : Pagination et différentiel
Objectif
Aller toucher à la requête HTTP en lecture et écriture avec la manipulation des QuerySet
et calcul du ETag.
Pour cette version, l'ETag ne sera pas sauvegardée et sera donc à chaque fois recalculée mais cela permettra d'avoir une version fonctionnelle et testable de l'ETag avant de statuer sur sa persistance.
v0.4 : Gestion de l'authentification.
Objectif
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 uneZDS_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>" } |
- 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"
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
POST | /v0.4/login/ | Aucun, le login et le passwod est contenu dans la requête | Authentification de l'utilisateur dans l'API | 400 si les headers Authbearer sont absents. 403 si login failed. |
GET | /v0.4/membres/monprofil | Aucun | Affiche toutes les données personnelles d'un utilisateur connecté | 401 : pour un utilisateur non identifié qui essaie d'accéder à sa page de profil. |
v0.5 : gestion du PUT
Objectifs
Interaction en écriture avec le code côté serveur
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
PUT | /v0.5/membres/{id} | Aucun, les paramètres se trouvent en contenu de la requêtes. | Utilisateur authentifié peut modifier sa fiche profil via l'API | 400 si un champ n'est pas formaté comme attendu (400x à définir : email invalide, …) s'inspirer de la gestion d'erreur déjà présente sur la page de profil. 401 si non authentifié. 403 si essai de modifier un profil autre que le sien. 404 si id du membre n'existe pas. 405 si un PUT est fait sur /membres/ puisqu'on ne peut pas modifier la liste des membres. |
v0.6 : gestion du POST
Objectif
Création de compte via l'API membres.
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
POST | /v0.6/membres/ | Aucun, les informations se trouvent en paramètre de la requête | Utilisateur non identifié peut se créer un compte | A définir |
Futur de l'API des membres (possiblement hors scope de cette ZEP)
v1.x : Gestion des paramètres de requête.
Objectif
Etablir la liaison entre la couche API et la couche d'accès aux données. Que peut-on reprendre du code utilisé actuellement pour les vues ? Peut on factoriser du code pour faire une page de recherche parmi les membres sur le site ?
Routes
Méthode | URL | Paramètres et signification | Description | Codes d'erreur |
---|---|---|---|---|
GET | /v0.7/membres/ | search | Possibilité de rechercher, filtrer, trier la liste des membres | Aucun autre que les codes standard |
v1.x : Gérer les associations dans le modèle renvoyé au client.
Objectif
Aller plus loin que "un modèle en base" <-> "un modèle renvoyé au client".
Ex : extraire un bout (l'url, le nom, …) d'un cours en bêta pour le mettre dans le contenu de la réponse envoyé lors de la consultation de la fiche d'un membre.