Carnets en ligne - Technologiehttps://blog.notmyidea.org/2016-02-11T00:00:00+01:00Let's Encrypt + HAProxy2016-02-11T00:00:00+01:002016-02-11T00:00:00+01:00tag:blog.notmyidea.org,2016-02-11:/lets-encrypt-haproxy.html<p><em>Note : Cet article n'est plus à jour. Il est maintenant (2018) possible d'installer des certificats SSL Let's Encrypt d'une manière beaucoup plus simple, en utilisant certbot (et le plugin nginx <code>certbot --nginx</code>).</em></p> <blockquote> <p>It’s time for the Web to take a big step forward in terms of security and privacy …</p></blockquote><p><em>Note : Cet article n'est plus à jour. Il est maintenant (2018) possible d'installer des certificats SSL Let's Encrypt d'une manière beaucoup plus simple, en utilisant certbot (et le plugin nginx <code>certbot --nginx</code>).</em></p> <blockquote> <p>It’s time for the Web to take a big step forward in terms of security and privacy. We want to see HTTPS become the default. Let’s Encrypt was built to enable that by making it as easy as possible to get and manage certificates.</p> <p>-- <a href="https://letsencrypt.org/">Let's Encrypt</a></p> </blockquote> <p>Depuis début Décembre, la nouvelle <em>autorité de certification</em> Let's Encrypt est passée en version <em>Beta</em>. Les certificats SSL sont un moyen de 1. chiffrer la communication entre votre navigateur et le serveur et 2. un moyen d'être sur que le site Web auquel vous accédez est celui auquel vous pensez vous connecter (pour éviter des <a href="https://fr.wikipedia.org/wiki/Attaque_de_l'homme_du_milieu">attaques de l'homme du milieu</a>).</p> <p>Jusqu'à maintenant, il était nécessaire de payer une entreprise pour faire en sorte d'avoir des certificats qui évitent d'avoir ce genre d'erreurs dans vos navigateurs:</p> <p><img alt="Message de firefox lorsque une connexion n'est pas sécurisée." src="%7Bfilename%7D/static/unsecure-connection.png"></p> <p>Maintenant, grâce à Let's Encrypt il est possible d'avoir des certificats SSL <strong>gratuits</strong>, ce qui représente un grand pas en avant pour la sécurité de nos communications.</p> <p>Je viens de mettre en place un procédé (assez simple) qui permet de configurer votre serveur pour générer des certificats SSL valides avec Let's Encrypt et le répartiteur de charge <a href="http://www.haproxy.org/">HAProxy</a>.</p> <p>Je me suis basé pour cet article sur d'<a href="https://blog.infomee.fr/p/letsencrypt-haproxy">autres</a> <a href="http://blog.victor-hery.com/article22/utiliser-let-s-encrypt-avec-haproxy">articles</a>, dont je vous recommande la lecture pour un complément d'information.</p> <h2>Validation des domaines par Let's Encrypt</h2> <p>Je vous passe les détails d'installation du client de Let's Encrypt, qui sont <a href="https://github.com/letsencrypt/letsencrypt#installation">très bien expliqués sur leur documentation</a>.</p> <p>Une fois installé, vous allez taper une commande qui va ressembler à:</p> <div class="highlight"><pre><span></span><span class="err">letsencrypt-auto certonly --renew-by-default</span> <span class="err">--webroot -w /home/www/letsencrypt-requests/ \</span> <span class="err">-d hurl.kinto-storage.org \</span> <span class="err">-d forums.kinto-storage.org</span> </pre></div> <p>Le <em>webroot</em> est l'endroit ou les preuves de détention du domaine vont être déposées.</p> <p>Lorsque les serveurs de Let's Encrypt vont vouloir vérifier que vous êtes bien à l'origine des demandes de certificats, ils vont envoyer une requête HTTP sur <code>http://domaine.org/.well-known/acme-challenge</code>, ou il voudra trouver des informations qu'il aura généré via la commande <code>letsencrypt-auto</code>.</p> <p>J'ai choisi de faire une règle dans haproxy pour diriger toutes les requêtes avec le chemin <code>.well-known/acme-challenge</code> vers un <em>backend</em> nginx qui sert des fichiers statiques (ceux contenus dans <code>/home/www/letsencrypt-requests/</code>).</p> <p>Voici la section de la configuration de HAProxy (et <a href="https://github.com/almet/infra/blob/master/haproxy/haproxy.cfg#L63-L72">la configuration complete</a> si ça peut être utile):</p> <div class="highlight"><pre><span></span><span class="n">frontend</span> <span class="n">http</span> <span class="n">bind</span> <span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">:</span><span class="mi">80</span> <span class="k">mode</span> <span class="n">http</span> <span class="n">default_backend</span> <span class="n">nginx_server</span> <span class="n">acl</span> <span class="n">letsencrypt_check</span> <span class="n">path_beg</span> <span class="o">/</span><span class="p">.</span><span class="n">well</span><span class="o">-</span><span class="n">known</span><span class="o">/</span><span class="n">acme</span><span class="o">-</span><span class="n">challenge</span> <span class="n">use_backend</span> <span class="n">letsencrypt_backend</span> <span class="k">if</span> <span class="n">letsencrypt_check</span> <span class="n">redirect</span> <span class="n">scheme</span> <span class="n">https</span> <span class="n">code</span> <span class="mi">301</span> <span class="k">if</span> <span class="o">!</span><span class="err">{</span> <span class="n">ssl_fc</span> <span class="err">}</span> <span class="o">!</span><span class="n">letsencrypt_check</span> <span class="n">backend</span> <span class="n">letsencrypt_backend</span> <span class="n">http</span><span class="o">-</span><span class="n">request</span> <span class="k">set</span><span class="o">-</span><span class="n">header</span> <span class="k">Host</span> <span class="n">letsencrypt</span><span class="p">.</span><span class="n">requests</span> <span class="k">dispatch</span> <span class="mi">127</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">:</span><span class="mi">8000</span> </pre></div> <p>Et celle de NGINX:</p> <div class="highlight"><pre><span></span><span class="err">server {</span> <span class="err"> listen 8000;</span> <span class="err"> server_name letsencrypt.requests;</span> <span class="err"> root /home/www/letsencrypt-requests;</span> <span class="err">}</span> </pre></div> <h2>Installation des certificats dans HAProxy</h2> <p>Vos certificats SSL devraient être générés dans <code>/etc/letsencrypt/live</code>, mais ils ne sont pas au format attendu par haproxy. Rien de grave, la commande suivant convertit l'ensemble des certificats en une version compatible avec HAProxy:</p> <div class="highlight"><pre><span></span><span class="err">cat /etc/letsencrypt/live/domaine.org/privkey.pem /etc/letsencrypt/live/domaine.org/fullchain.pem &gt; /etc/ssl/letsencrypt/domaine.org.pem</span> </pre></div> <p>Et ensuite dans la configuration de haproxy, pour le (nouveau) <em>frontend</em> https:</p> <div class="highlight"><pre><span></span><span class="err">bind 0.0.0.0:443 ssl no-sslv3 crt /etc/ssl/letsencrypt</span> </pre></div> <p>Faites bien attention à avoir un <em>frontend</em> https pour tous vos sites en HTTPS. <a href="https://github.com/almet/infra/blob/master/haproxy/haproxy.cfg#L38-L60">Pour moi cela ressemble à ça</a>.</p> <p>Une fois tout ceci fait, redémarrez votre service haproxy et zou !</p> <h2>Automatisation</h2> <p>Pour automatiser un peu tout ça, j'ai choisi de faire ça comme suit:</p> <ul> <li>Un fichier domaine dans <code>letsencrypt/domains/domain.org</code> qui contient le script <code>letsencrypt</code>.</li> <li>Un fichier d'installation de certificats dans <code>letsencrypt/install-certs.sh</code> qui s'occupe d'installer les certificats déjà générés.</li> </ul> <p>Et voila ! <a href="https://github.com/almet/infra/">Le tout est dans un dépot github</a>, si jamais ça peut vous servir, tant mieux !</p>Ateliers d'autodéfense numérique2016-01-14T00:00:00+01:002016-01-14T00:00:00+01:00tag:blog.notmyidea.org,2016-01-14:/ateliers-dautodefense-numerique.html<p>Il y a huit mois, je me rendais compte de l'importance du choix des outils pour faire face à la surveillance généralisée, et notamment en rapport au chiffrement des données. Une de mes envies de l'époque était l'animation d'ateliers.</p> <blockquote> <p>Je compte donc:</p> <ul> <li>Organiser des ateliers de sensibilisation aux outils de …</li></ul></blockquote><p>Il y a huit mois, je me rendais compte de l'importance du choix des outils pour faire face à la surveillance généralisée, et notamment en rapport au chiffrement des données. Une de mes envies de l'époque était l'animation d'ateliers.</p> <blockquote> <p>Je compte donc:</p> <ul> <li>Organiser des ateliers de sensibilisation aux outils de communication, envers mes proches;</li> <li>Utiliser la communication chiffrée le plus souvent possible, au moins pour rendre le déchiffrement des messages plus longue, "noyer le poisson".</li> </ul> <p>-- <a href="http://blog.notmyidea.org/chiffrement.html">Chiffrement</a></p> </blockquote> <p>J'ai mis un peu de temps à mettre le pied à l'étrier, mais je ressors finalement du premier atelier que j'ai co-animé avec geb, auprès d'un public de journalistes.</p> <p>Pour cette première édition l'idée était à la fois d'aller à la rencontre d'un public que je connais mal, de leur donner des outils pour solutionner les problèmes auxquels ils font parfois face, et de me faire une idée de ce que pouvait être un atelier sur l'autodéfense numérique.</p> <p>L'objectif pour ce premier atelier était de:</p> <ol> <li>Échanger autour des besoins et <strong>faire ressortir des histoires</strong> ou le manque d'outillage / connaissances à posé problème, dans des situations concrètes;</li> <li>Se rendre compte des "conduites à risque", <strong>faire peur</strong> aux personnes formées pour qu'elles se rendent compte de l'état actuel des choses;</li> <li><strong>Proposer des solutions concrètes</strong> aux problèmes soulevés, ainsi que le minimum de connaissance théorique pour les appréhender.</li> </ol> <h2>1. Faire ressortir les problèmes</h2> <p>Afin de faire ressortir les problèmes, nous avons choisi de constituer des petits groupes de discussion, afin de faire des "Groupes d'Interview Mutuels", ou "GIM":</p> <blockquote> <p>l’animateur invite les participants à se regrouper par trois, avec des personnes qu’on connaît moins puis invite chacun à livrer une expérience vécue en lien avec le thème de la réunion et les deux autres à poser des questions leur permettant de bien saisir ce qui a été vécu.</p> <p>-- «<a href="http://www.scoplepave.org/pour-s-ecouter">Pour s'écouter</a>», SCOP Le Pavé.</p> </blockquote> <p>De ces <em>GIMs</em> nous avons pu ressortir quelques histoires, gravitant autour de:</p> <ul> <li><strong>La protection des sources (d'information)</strong>: Comment faire pour aider quelqu'un à faire "fuiter" des données depuis l'intérieur d'une entreprise ?</li> <li><strong>Le chiffrement de ses données</strong>: Comment éviter de faire "fuiter" des données importantes lors d'une perquisition de matériel ?</li> </ul> <h2>2. Faire peur</h2> <p>Un des premiers objectifs est de faire peur, afin que tout le monde se rende compte à quel point il est facile d'accéder à certaines données. <a href="http://blog.barbayellow.com/">Grégoire</a> m'avait conseillé quelques petites accroches qui ont ma foi bien marché:</p> <p>J'ai demandé aux présent.e.s de:</p> <ul> <li>donner leur mot de passe à voix haute devant les autres: a priori personne ne le fera;</li> <li>venir se connecter à leur compte email depuis mon ordinateur. J'ai piégé une personne, qui est venu pour taper son mot de passe.</li> </ul> <p>Cela à été un bon moyen de parler de l'importance des traces que l'on peut laisser sur un ordinateur, et de la confiance qu'il faut avoir dans le matériel que l'on utilise, à fortiori si ce ne sont pas les vôtres.</p> <p>Pour continuer à leur faire peur, après une brève explication de ce qu'est SSL nous avons montré comment il était facile de scruter le réseau à la recherche de mots de passe en clair.</p> <h2>3. Proposer des solutions concrêtes</h2> <p>Une fois que tout le monde avait pleinement pris sonscience des problématiques et n'osait plus utiliser son ordinateur ou son téléphone, on à commencé à parler de quelques solutions. Plusieurs approches étaient possibles ici, nous avons choisi de présenter quelques outils qui nous semblaient répondre aux attentes:</p> <ul> <li>On a expliqué ce qu'était <a href="https://tails.boum.org">Tails</a>, et comment l'utiliser et le dupliquer.</li> <li>On a pu faire un tour des outils existants sur Tails, notamment autour de l'<em>anonymisation</em> de fichiers et la suppression effective de contenus.</li> <li>Certaines personnes ont pu créer une clé tails avec la persistance de configurée.</li> <li>Nous nous sommes connectés au réseau <a href="https://www.torproject.org">Tor</a> et testé que nos adresses IP changeaient bien à la demande.</li> <li>Nous avons utilisé <a href="https://crypto.cat">CryptoCat</a> par dessus Tor, afin de voir comment avoir une conversation confidentielle dans laquelle il est possible d'échanger des fichiers.</li> </ul> <h2>Retours</h2> <p>D'une manière générale, pour une formation de trois heures et demi, je suis assez content de l'exercice, et de l'ensemble des sujets que nous avons pu couvrir. Il y a beaucoup de place pour l'amélioration, notamment en amont (j'avais par exemple oublié d'amener avec moi suffisamment de clés USB pour utiliser Tails).</p> <p>La plupart des retours qu'on a pu avoir jusqu'à maintenant sont positifs, et il y a l'envie d'aller plus loin sur l'ensemble de ces sujets.</p> <h2>La suite</h2> <p>Il y a beaucoup de sujets que nous n'avons pas abordés, ou uniquement survolés, à cause du manque de temps disponible. Idéalement, il faudrait au moins une journée entière pour couvrir quelques sujets plus en détail (on peut imaginer avoir une partie théorique le matin et une partie pratique l'après-midi par exemple).</p> <p>J'ai choisi volontairement de ne pas aborder le chiffrement des messages via PGP parce que <a href="%7Bfilename%7D2015.05.pgp-problemes.rst">je pense que la protection que ce média propose n'est pas suffisante</a>, mais je suis en train de revenir sur ma décision: il pourrait être utile de présenter l'outil, à minima, en insistant sur certaines de ses faiblesses.</p> <p>Un compte twitter à été créé recemment autour des crypto-party à Rennes, si vous êtes interessés, <a href="https://twitter.com/CryptoPartyRNS">allez jeter un coup d'œil</a>!</p> <p>Je n'ai pas trouvé de ressources disponibles par rapport à des plans de formation sur le sujet, j'ai donc décidé de publier les nôtres, afin de co-construire avec d'autres des plans de formation.</p> <p>Ils sont pour l'instant disponibles <a href="http://autodefense-numerique.readthedocs.org/en/latest/">sur Read The Docs</a>. Tous les retours sont évidemment les bienvenus !</p>Le mail doit-il mourir ?2015-11-24T00:00:00+01:002015-11-24T00:00:00+01:00tag:blog.notmyidea.org,2015-11-24:/le-mail-doit-il-mourir.html<p>J'utilise quotidiennement le protocole email, tant bien que mal, tout en sachant que l'ensemble de mes messages passent en clair sur le réseau pour la plupart de mes conversations, puisque trop peu de monde utilise le chiffrement des messages.</p> <p>Et même si j'arrive à convaincre certains de mes proches à …</p><p>J'utilise quotidiennement le protocole email, tant bien que mal, tout en sachant que l'ensemble de mes messages passent en clair sur le réseau pour la plupart de mes conversations, puisque trop peu de monde utilise le chiffrement des messages.</p> <p>Et même si j'arrive à convaincre certains de mes proches à installer PGP, je ne suis pas satisfait du résultat: les méta-données (qui contacte qui à quel moment, et pour lui dire quoi) transitent de toute manière, elles, en clair, à la vue de tous.</p> <p>Ce problème est lié directement au protocole email: il est <em>necessaire</em> de faire fuiter ces meta-données (au moins le destinataire) pour avoir un protocole mail fonctionnel.</p> <p>Le mail répond à un besoin de communication asynchrone qui permet des conversations plus réfléchies qu'un simple chat (miaou). Il est tout à fait possible d'utiliser certaines technologies existantes afin de construire le futur de l'email, pour lequel:</p> <ul> <li>Les méta-données seraient chiffrées — Il n'est pas possible de savoir qui communique avec qui, et quand;</li> <li>Le chiffrement serait fort (et protégé d'une phrase de passe ?);</li> <li>La fuite d'une clé de chiffrement utilisée dans un échange ne permette pas de déchiffrer l'ensemble des échanges (forward secrecy);</li> <li>Il ne soit pas possible de réutiliser les données comme preuve pour incriminer l'emmeteur du message (deniability);</li> </ul> <p>Avec au moins ces besoins en tête, il semble qu'une revue de l'ensemble des projets existants pointe du doigt vers <a href="https://github.com/agl/pond">pond</a>, ou vers <a href="https://www.whispersystems.org">Signal</a>.</p> <p>Malheureusement, Pond est le projet d'une seule personne, qui veut plutôt utiliser ce code comme démonstration du concept en question.</p>Web distribution signing2015-10-12T00:00:00+02:002015-10-12T00:00:00+02:00tag:blog.notmyidea.org,2015-10-12:/web-distribution-signing.html<p><em>I'm not a crypto expert, nor pretend to be one. These are thoughts I want to share with the crypto community to actually see if any solution exists to solve this particular problem.</em></p> <p>One <a href="http://www.tonyarcieri.com/whats-wrong-with-webcrypto">often pointed</a> flaw in web-based cryptographic applications is the fact that there is no way to …</p><p><em>I'm not a crypto expert, nor pretend to be one. These are thoughts I want to share with the crypto community to actually see if any solution exists to solve this particular problem.</em></p> <p>One <a href="http://www.tonyarcieri.com/whats-wrong-with-webcrypto">often pointed</a> flaw in web-based cryptographic applications is the fact that there is no way to trust online software distributions. Put differently, you don't actually trust the software authors but are rather trusting the software distributors and certificate authorities (CAs).</p> <p>I've been talking with a few folks in the past months about that and they suggested me to publish something to discuss the matter. So here I come!</p> <h2>The problem (Attack vectors)</h2> <p>Let's try to describe a few potential attacks:</p> <p><em>Application Authors</em> just released a new version of their open source web crypto messaging application. An <em>Indie Hoster</em> installs it on their servers so a wide audience can actually use it.</p> <p>Someone alters the files on <em>Indie Hoster</em> servers, effectively replacing them with other <em>altered files</em> with less security properties / a backdoor. This someone could either be an <em>Evil Attacker</em> which found its way trough, the <em>Indie Hoster</em> or a CDN which delivers the files,</p> <p>Trusted <em>Certificate Authorities</em> ("governments" or "hacking team") can also trick the User Agents (i.e. Firefox) into thinking they're talking to <em>Indie Hoster</em> even though they're actually talking to a different server.</p> <p><strong>Altered files</strong> are then being served to the User Agents, and <em>Evil Attacker</em> now has a way to actually attack the end users.</p> <h2>Problem Mitigation</h2> <p>Part of the problem is solved by the recently introduced <a href="https://w3c.github.io/webappsec/specs/subresourceintegrity/">Sub Resource Integrity</a> (SRI). To quote them: "[it] defines a mechanism by which user agents may verify that a fetched resource has been delivered without unexpected manipulation.".</p> <p>SRI is a good start, but isn't enough: it ensures the assets (JavaScript files, mainly) loaded from a specific HTML page are the ones the author of the HTML page intends. However, SRI doesn't allow the User Agent to ensure the HTML page is the one he wants.</p> <p>In other words, we miss a way to create trust between <em>Application Authors</em> and <em>User Agents</em>. The User-Agent currently has to trust the <em>Certificate Authorities</em> and the delivery (<em>Indie Hoster</em>).</p> <p>For desktop software distribution: <em>Crypto Experts</em> audit the software, sign it somehow and then this signature can be checked locally during installation or runtime. It's not automated, but at least it's possible.</p> <p>For web applications, we don't have such a mechanism, but it should be possible. Consider the following:</p> <ul> <li><em>App Authors</em> publish a new version of their software; They provide a hash of each of their distributed files (including the HTML files);</li> <li><em>Crypto Experts</em> audit these files and sign the hashes somehow;</li> <li><em>User Agents</em> can chose to trust some specific <em>Crypto Experts</em>;</li> <li>When a <em>User Agent</em> downloads files, it checks if they're signed by a trusted party.</li> </ul> <h2>Chosing who you trust</h2> <p>In terms of user experience, handling certificates is hard, and that's where the community matters. Distributions such as <a href="https://tails.boom.org">Tails</a> could chose who they trust to verify the files, and issue warnings / refuse to run the application in case files aren't verified.</p> <p>But, as highligted earlier, CAs are hard to trust. A new instance of the same CA system wouldn't make that much differences, expect the fact that distributions could ship with a set of trusted authorities (for which revocation would still need to be taken care of).</p> <blockquote> <p>[...] users are vulnerable to MitM attacks by the authority, which can vouch for, or be coerced to vouch for, false keys. This weakness has been highlighted by recent CA scandals. Both schemes can also be attacked if the authority does not verify keys before vouching for them.</p> <p>-- <a href="http://cacr.uwaterloo.ca/techreports/2015/cacr2015-02.pdf">SoK : Secure Messaging</a>;</p> </blockquote> <p>It seems that some other systems could allow for something more reliable:</p> <blockquote> <p>Melara et al proposed CONIKS, using a series of chained commitments to Merkle prefix trees to build a key directory [...] for which individual users can efficiently verify the consistency of their own entry in the directory without relying on a third party.</p> <p>This “self- auditing log” approach makes the system partially have no auditing required (as general auditing of non-equivocation is still required) and also enables the system to be privacy preserving as the entries in the directory need not be made public. This comes at a mild bandwidth cost not reflected in our table, estimated to be about 10 kilobytes per client per day for self-auditing.</p> <p>-- <a href="http://cacr.uwaterloo.ca/techreports/2015/cacr2015-02.pdf">SoK : Secure Messaging</a>;</p> </blockquote> <p>Now, I honestly have no idea if this thing solves the whole problem, and I'm pretty sure this design has many security problems attached to it.</p> <p>However, that's a problem I would really like to see solved one day, so here the start of the discussion, don't hesitate to <a href="/pages/about.html">get in touch</a>!</p> <h2>Addendum</h2> <p>It seems possible to increase the level a user has in a Web Application by adding indicators in the User-Agent. For instance, when using an application that's actually signed by someone considered trustful by the User-Agent (or the distributor of the User-Agent), a little green icon could be presented to the User, so they know that they can be confident about this.</p> <p>A bit like User-Agents do for SSL, but for the actual signature of the files being viewed.</p>Service de nuages : Pourquoi avons-nous fait Cliquet ?2015-07-14T00:00:00+02:002015-07-14T00:00:00+02:00tag:blog.notmyidea.org,2015-07-14:/pourquoi-cliquet<p class="first last">Basé sur Pyramid, Cliquet est un projet qui permet de se concentrer sur l'essentiel lors de la conception d'APIs.</p> <p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p> <p><strong>tldr; Cliquet est un toolkit Python pour construire des APIs, qui implémente les bonnes pratiques en terme de mise en production et de protocole HTTP.</strong></p> <div class="section" id="les-origines"> <h2>Les origines</h2> <p>L'objectif pour le premier trimestre 2015 était de construire un service de stockage et de <a class="reference external" href="{filename}2015.04.service-de-nuages.rst">synchronisation de listes de lecture</a>.</p> <p>Au démarrage du projet, nous avons tenté de rassembler toutes les bonnes pratiques et recommandations, venant de différentes équipes et surtout des derniers projets déployés.</p> <p>De même, nous voulions tirer parti du protocole de <em>Firefox Sync</em>, robuste et éprouvé, pour la synchronisation des données «offline».</p> <p>Plutôt qu'écrire un <a class="reference external" href="http://blog.octo.com/en/design-a-rest-api/">énième</a> <a class="reference external" href="http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api">article</a> de blog, nous avons préféré les rassembler dans ce qu'on a appellé «un protocole».</p> <p>Comme pour l'architecture envisagée nous avions deux projets à construire, qui devaient obéir globalement à ces mêmes règles, nous avons décidé de mettre en commun l'implémentation de ce protocole et de ces bonnes pratiques dans un «toolkit».</p> <p><em>Cliquet</em> est né.</p> <img alt="Cliquet logo" class="align-center" src="{filename}/images/cliquet-logo.png" /> <div class="section" id="les-intentions"> <h3>Les intentions</h3> <blockquote class="epigraph"> Quelle structure JSON pour mon API ? Quelle syntaxe pour filtrer la liste via la querystring ? Comment gérer les écritures concurrentes ? Et synchroniser les données dans mon application cliente ?</blockquote> <p>Désormais, quand un projet souhaite bénéficier d'une API REST pour stocker et consommer des données, il est possible d'utiliser le <strong>protocole HTTP</strong> proposé et de se concentrer sur l'essentiel. Cela vaut aussi pour les clients, où la majorité du code d'interaction avec le serveur est réutilisable.</p> <blockquote class="epigraph"> Comment pouvons-nous vérifier que le service est opérationnel ? Quels indicateurs StatsD ? Est-ce que Sentry est bien configuré ? Comment déployer une nouvelle version sans casser les applications clientes ?</blockquote> <p>Comme <em>Cliquet</em> fournit tout ce qui est nécessaire pour être conforme avec les exigences de la <strong>mise en production</strong>, le passage du prototype au service opérationnel est très rapide ! De base le service répondra aux attentes en terme supervision, configuration, déploiement et dépréciation de version. Et si celles-ci évoluent, il suffira de faire évoluer le toolkit.</p> <blockquote class="epigraph"> Quel backend de stockage pour des documents JSON ? Comment faire si l'équipe de production impose PostgreSQL ? Et si on voulait passer à Redis ou en mémoire pour lancer les tests ?</blockquote> <p>En terme d'implémentation, nous avons choisi de <strong>fournir des abstractions</strong>. En effet, nous avions deux services dont le coeur consistait à exposer un <em>CRUD</em> en <em>REST</em>, persistant des données JSON dans un backend. Comme <em>Pyramid</em> et <em>Cornice</em> ne fournissent rien de tout prêt pour ça, nous avons voulu introduire des classes de bases pour abstraire les notions de resource REST et de backend de stockage.</p> <p>Dans le but de tout rendre optionnel et «pluggable», <strong>tout est configurable</strong> depuis le fichier <tt class="docutils literal">.ini</tt> de l'application. Ainsi tous les projets qui utilisent le toolkit se déploieront de la même manière : seuls quelques éléments de configuration les distingueront.</p> <img alt="Une réunion à Paris..." class="align-center" src="{filename}/images/cliquet-notes-whiteboard.jpg" /> </div> </div> <div class="section" id="le-protocole"> <h2>Le protocole</h2> <blockquote class="epigraph"> Est-ce suffisant de parler d'«API REST» ? Est-ce bien nécessaire de relire la spec HTTP à chaque fois ? Pourquoi réinventer un protocole complet à chaque fois ?</blockquote> <p>Quand nous développons un (micro)service Web, nous dépensons généralement beaucoup trop d'énergie à (re)faire des choix (arbitraires).</p> <p>Nul besoin de lister ici tout ce qui concerne la dimension de la spécification HTTP pure, qui nous impose le format des headers, le support de CORS, la négocation de contenus (types mime), la différence entre authentification et autorisation, la cohérence des code status...</p> <p>Les choix principaux du protocole concernent surtout :</p> <ul class="simple"> <li><strong>Les resources REST</strong> : Les deux URLs d'une resource (pour la collection et les enregistrements) acceptent des verbes et des headers précis.</li> <li><strong>Les formats</strong> : le format et la structure JSON des réponses est imposé, ainsi que la <a class="reference external" href="{filename}/2015.05.continuation-token.rst">pagination des listes</a> ou la syntaxe pour filtrer/trier les resources via la <a class="reference external" href="https://en.wikipedia.org/wiki/Query_string">querystring</a>.</li> <li><strong>Les timestamps</strong> : un numéro de révision qui s'incrémente à chaque opération d'écriture sur une collection d'enregistrements.</li> <li><strong>La synchronisation</strong> : une série de leviers pour récupérer et renvoyer des changements sur les données, sans perte ni collision, en utilisant les timestamps.</li> <li><strong>Les permissions</strong> : les droits d'un utilisateur sur une collection ou un enregistrement (<em>encore frais et sur le point d'être documenté</em>) <a class="footnote-reference" href="#id3" id="id1">[1]</a>.</li> <li><strong>Opérations par lot</strong>: une URL qui permet d'envoyer une série de requêtes décrites en JSON et d'obtenir les réponses respectives.</li> </ul> <p>Dans la dimension opérationnelle du protocole, on trouve :</p> <ul class="simple"> <li><strong>La gestion de version</strong> : cohabitation de plusieurs versions en production, avec alertes dans les entêtes pour la fin de vie des anciennes versions.</li> <li><strong>Le report des requêtes</strong> : entêtes interprétées par les clients, activées en cas de maintenance ou de surchage, pour ménager le serveur.</li> <li><strong>Le canal d'erreurs</strong> : toutes les erreurs renvoyées par le serveur ont le même format JSON et ont un numéro précis.</li> <li><strong>Les utilitaires</strong> : URLs diverses pour répondre aux besoins exprimés par l'équipe d'administrateurs (monitoring, metadonnées, paramètres publiques).</li> </ul> <p>Ce protocole est une compilation des bonnes pratiques pour les APIs HTTP (<em>c'est notre métier !</em>), des conseils des administrateurs système dont c'est le métier de mettre à disposition des services pour des millions d'utilisateurs et des retours d'expérience de l'équipe de <em>Firefox Sync</em> pour la gestion de la concurrence et de l'«offline-first».</p> <p>Il est <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/api/index.html">documenté en détail</a>.</p> <p>Dans un monde idéal, ce protocole serait versionné, et formalisé dans une RFC. En rêve, il existerait même plusieurs implémentations avec des technologies différentes (Python, Go, Node, etc.). <a class="footnote-reference" href="#id4" id="id2">[2]</a></p> <table class="docutils footnote" frame="void" id="id3" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id1">[1]</a></td><td>Voir notre <a class="reference external" href="{filename}/2015.05.cliquet-permissions.rst">article dédié sur les permissions</a></td></tr> </tbody> </table> <table class="docutils footnote" frame="void" id="id4" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id2">[2]</a></td><td>Rappel: nous sommes une toute petite équipe !</td></tr> </tbody> </table> </div> <div class="section" id="le-toolkit"> <h2>Le toolkit</h2> <div class="section" id="choix-techniques"> <h3>Choix techniques</h3> <p><em>Cliquet</em> implémente le protocole en Python (<em>2.7, 3.4+, pypy</em>), avec <a class="reference external" href="http://trypyramid.com/">Pyramid</a> <a class="footnote-reference" href="#id6" id="id5">[3]</a>.</p> <p><strong>Pyramid</strong> est un framework Web qui va prendre en charge tout la partie HTTP, et qui s'avère pertinent aussi bien pour des petits projets que des plus ambitieux.</p> <p><strong>Cornice</strong> est une extension de <em>Pyramid</em>, écrite en partie par Alexis et Tarek, qui permet d'éviter d'écrire tout le code <em>boilerplate</em> quand on construit une API REST avec Pyramid.</p> <p>Avec <em>Cornice</em>, on évite de réécrire à chaque fois le code qui va cabler les verbes HTTP aux méthodes, valider les entêtes, choisir le sérialiseur en fonction des entêtes de négociation de contenus, renvoyer les codes HTTP rigoureux, gérer les entêtes CORS, fournir la validation JSON à partir de schémas...</p> <p><strong>Cliquet</strong> utilise les deux précédents pour implémenter le protocole et fournir des abstractions, mais on a toujours <em>Pyramid</em> et <em>Cornice</em> sous la main pour aller au delà de ce qui est proposé !</p> <table class="docutils footnote" frame="void" id="id6" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id5">[3]</a></td><td>Au tout début nous avons commencé une implémentation avec <em>Python-Eve</em> (Flask), mais n'étions pas satisfaits de l'approche pour la configuration de l'API. En particulier du côté magique.</td></tr> </tbody> </table> </div> <div class="section" id="concepts"> <h3>Concepts</h3> <p>Bien évidemment, les concepts du toolkit reflètent ceux du protocole mais il y a des éléments supplémentaires:</p> <ul class="simple"> <li><strong>Les backends</strong> : abstractions pour le stockage, le cache et les permissions (<em>ex. PostgreSQL, Redis, en-mémoire, ...</em>)</li> <li><strong>La supervision</strong> : logging JSON et indicateurs temps-réel (<em>StatsD</em>) pour suivre les performances et la santé du service.</li> <li><strong>La configuration</strong> : chargement de la configuration depuis les variables d'environnement et le fichier <tt class="docutils literal">.ini</tt></li> <li><strong>La flexibilité</strong> : dés/activation ou substitution de la majorité des composants depuis la configuration.</li> <li><strong>Le profiling</strong> : utilitaires de développement pour trouver les <a class="reference external" href="https://fr.wiktionary.org/wiki/goulet_d%E2%80%99%C3%A9tranglement">goulets d'étranglement</a>.</li> </ul> <img alt="Cliquet concepts" class="align-center" src="{filename}/images/cliquet-concepts.png" /> <p>Proportionnellement, l'implémentation du protocole pour les resources REST est la plus volumineuse dans le code source de <em>Cliquet</em>. Cependant, comme nous l'avons décrit plus haut, <em>Cliquet</em> fournit tout un ensemble d'outillage et de bonnes pratiques, et reste donc tout à fait pertinent pour n'importe quel type d'API, même sans manipulation de données !</p> <p>L'objectif de la boîte à outils est de faire en sorte qu'un développeur puisse constuire une application simplement, en étant sûr qu'elle réponde aux exigeances de la mise en production, tout en ayant la possibilité de remplacer certaines parties au fur et à mesure que ses besoins se précisent.</p> <p>Par exemple, la persistence fournie par défault est <em>schemaless</em> (e.g <em>JSONB</em>), mais rien n'empêcherait d'implémenter le stockage dans un modèle relationnel.</p> <p>Comme les composants peuvent être remplacés depuis la configuration, il est tout à fait possible d'étendre <em>Cliquet</em> avec des notions métiers ou des technologies exotiques ! Nous avons posé quelques idées dans <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/ecosystem.html">la documentation de l'éco-système</a>.</p> <p>Dans les prochaines semaines, nous allons introduire la notion d'«évènements» (ou signaux), qui permettraient aux extensions de s'interfacer beaucoup plus proprement.</p> <p>Nous attachons beaucoup d'importance à la clareté du code, la pertinence des <em>patterns</em>, des tests et de la documentation. Si vous avez des commentaires, des critiques ou des interrogations, n'hésitez pas à <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues">nous en faire part</a> !</p> </div> </div> <div class="section" id="cliquet-a-l-action"> <h2>Cliquet, à l'action.</h2> <p>Nous avons écrit un <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/quickstart.html">guide de démarrage</a>, qui n'exige pas de connaître <em>Pyramid</em>.</p> <p>Pour illustrer la simplicité et les concepts, voici quelques extraits !</p> <div class="section" id="etape-1"> <h3>Étape 1</h3> <p>Activer <em>Cliquet</em>:</p> <div class="highlight"><pre><span></span><span class="hll"><span class="kn">import</span> <span class="nn">cliquet</span> </span><span class="kn">from</span> <span class="nn">pyramid.config</span> <span class="kn">import</span> <span class="n">Configurator</span> <span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">global_config</span><span class="p">,</span> <span class="o">**</span><span class="n">settings</span><span class="p">):</span> <span class="n">config</span> <span class="o">=</span> <span class="n">Configurator</span><span class="p">(</span><span class="n">settings</span><span class="o">=</span><span class="n">settings</span><span class="p">)</span> <span class="hll"> <span class="n">cliquet</span><span class="o">.</span><span class="n">initialize</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="s1">&#39;1.0&#39;</span><span class="p">)</span> </span> <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="n">make_wsgi_app</span><span class="p">()</span> </pre></div> <p>À partir de là, la plupart des outils de <em>Cliquet</em> sont activés et accessibles.</p> <p>Par exemple, les URLs <em>hello</em> (<tt class="docutils literal">/v1/</tt>) ou <em>supervision</em> (<tt class="docutils literal">/v1/__heartbeat__</tt>). Mais aussi les backends de stockage, de cache, etc. qu'il est possible d'utiliser dans des vues classiques <em>Pyramid</em> ou <em>Cornice</em>.</p> </div> <div class="section" id="etape-2"> <h3>Étape 2</h3> <p>Ajouter des vues:</p> <div class="highlight"><pre><span></span><span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">global_config</span><span class="p">,</span> <span class="o">**</span><span class="n">settings</span><span class="p">):</span> <span class="n">config</span> <span class="o">=</span> <span class="n">Configurator</span><span class="p">(</span><span class="n">settings</span><span class="o">=</span><span class="n">settings</span><span class="p">)</span> <span class="n">cliquet</span><span class="o">.</span><span class="n">initialize</span><span class="p">(</span><span class="n">config</span><span class="p">,</span> <span class="s1">&#39;1.0&#39;</span><span class="p">)</span> <span class="hll"> <span class="n">config</span><span class="o">.</span><span class="n">scan</span><span class="p">(</span><span class="s2">&quot;myproject.views&quot;</span><span class="p">)</span> </span> <span class="k">return</span> <span class="n">config</span><span class="o">.</span><span class="n">make_wsgi_app</span><span class="p">()</span> </pre></div> <p>Pour définir des resources CRUD, il faut commencer par définir un schéma, avec <em>Colander</em>, et ensuite déclarer une resource:</p> <div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">cliquet</span> <span class="kn">import</span> <span class="n">resource</span><span class="p">,</span> <span class="n">schema</span> <span class="k">class</span> <span class="nc">BookmarkSchema</span><span class="p">(</span><span class="n">schema</span><span class="o">.</span><span class="n">ResourceSchema</span><span class="p">):</span> <span class="n">url</span> <span class="o">=</span> <span class="n">schema</span><span class="o">.</span><span class="n">URL</span><span class="p">()</span> <span class="hll"><span class="nd">@resource</span><span class="o">.</span><span class="n">register</span><span class="p">()</span> </span><span class="hll"><span class="k">class</span> <span class="nc">Bookmark</span><span class="p">(</span><span class="n">resource</span><span class="o">.</span><span class="n">BaseResource</span><span class="p">):</span> </span><span class="hll"> <span class="n">mapping</span> <span class="o">=</span> <span class="n">BookmarkSchema</span><span class="p">()</span> </span></pre></div> <p>Désormais, la resource CRUD est disponible sur <tt class="docutils literal">/v1/bookmarks</tt>, avec toutes les fonctionnalités de synchronisation, filtrage, tri, pagination, timestamp, etc. De base les enregistrements sont privés, par utilisateur.</p> <div class="highlight"><pre><span></span><span class="err">$</span> <span class="err">http</span> <span class="err">GET</span> <span class="s2">&quot;http://localhost:8000/v1/bookmarks&quot;</span> <span class="err">HTTP/</span><span class="mf">1.1</span> <span class="mi">200</span> <span class="err">OK</span> <span class="err">...</span> <span class="p">{</span> <span class="nt">&quot;data&quot;</span><span class="p">:</span> <span class="p">[</span> <span class="p">{</span> <span class="nt">&quot;url&quot;</span><span class="p">:</span> <span class="s2">&quot;http://cliquet.readthedocs.org&quot;</span><span class="p">,</span> <span class="nt">&quot;id&quot;</span><span class="p">:</span> <span class="s2">&quot;cc103eb5-0c80-40ec-b6f5-dad12e7d975e&quot;</span><span class="p">,</span> <span class="nt">&quot;last_modified&quot;</span><span class="p">:</span> <span class="mi">1437034418940</span><span class="p">,</span> <span class="p">}</span> <span class="p">]</span> <span class="p">}</span> </pre></div> </div> <div class="section" id="etape-3"> <h3>Étape 3</h3> <p>Évidemment, il est possible choisir les URLS, les verbes HTTP supportés, de modifier des champs avant l'enregistrement, etc.</p> <div class="highlight"><pre><span></span><span class="hll"><span class="nd">@resource</span><span class="o">.</span><span class="n">register</span><span class="p">(</span><span class="n">collection_path</span><span class="o">=</span><span class="s1">&#39;/user/bookmarks&#39;</span><span class="p">,</span> </span><span class="hll"> <span class="n">record_path</span><span class="o">=</span><span class="s1">&#39;/user/bookmarks/{{id}}&#39;</span><span class="p">,</span> </span><span class="hll"> <span class="n">collection_methods</span><span class="o">=</span><span class="p">(</span><span class="s1">&#39;GET&#39;</span><span class="p">,))</span> </span><span class="k">class</span> <span class="nc">Bookmark</span><span class="p">(</span><span class="n">resource</span><span class="o">.</span><span class="n">BaseResource</span><span class="p">):</span> <span class="n">mapping</span> <span class="o">=</span> <span class="n">BookmarkSchema</span><span class="p">()</span> <span class="hll"> <span class="k">def</span> <span class="nf">process_record</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">new</span><span class="p">,</span> <span class="n">old</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span> </span><span class="hll"> <span class="k">if</span> <span class="n">old</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">new</span><span class="p">[</span><span class="s1">&#39;device&#39;</span><span class="p">]</span> <span class="o">!=</span> <span class="n">old</span><span class="p">[</span><span class="s1">&#39;device&#39;</span><span class="p">]:</span> </span><span class="hll"> <span class="n">device</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;User-Agent&#39;</span><span class="p">)</span> </span><span class="hll"> <span class="n">new</span><span class="p">[</span><span class="s1">&#39;device&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="n">device</span> </span><span class="hll"> <span class="k">return</span> <span class="n">new</span> </span></pre></div> <p><a class="reference external" href="http://cliquet.readthedocs.org/en/latest/reference/resource.html">Plus d'infos dans la documentation dédiée</a> !</p> <div class="admonition note"> <p class="first admonition-title">Note</p> <p class="last">Il est possible de définir des resources sans validation de schema. <a class="reference external" href="https://github.com/mozilla-services/kinto/blob/master/kinto/views/records.py">Voir le code source de Kinto</a>.</p> </div> </div> <div class="section" id="etape-4-optionelle"> <h3>Étape 4 (optionelle)</h3> <p>Utiliser les abstractions de <em>Cliquet</em> dans une vue <em>Cornice</em>.</p> <p>Par exemple, une vue qui utilise le backend de stockage:</p> <div class="highlight"><pre><span></span><span class="kn">from</span> <span class="nn">cliquet</span> <span class="kn">import</span> <span class="n">Service</span> <span class="n">score</span> <span class="o">=</span> <span class="n">Service</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&quot;score&quot;</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="s1">&#39;/score/</span><span class="si">{game}</span><span class="s1">&#39;</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&quot;Store game score&quot;</span><span class="p">)</span> <span class="nd">@score</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="n">schema</span><span class="o">=</span><span class="n">ScoreSchema</span><span class="p">)</span> <span class="k">def</span> <span class="nf">post_score</span><span class="p">(</span><span class="n">request</span><span class="p">):</span> <span class="n">collection_id</span> <span class="o">=</span> <span class="s1">&#39;scores-&#39;</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="n">match_dict</span><span class="p">[</span><span class="s1">&#39;game&#39;</span><span class="p">]</span> <span class="n">user_id</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">authenticated_userid</span> <span class="n">value</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">validated</span> <span class="c1"># c.f. Cornice.</span> <span class="hll"> <span class="n">storage</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">registry</span><span class="o">.</span><span class="n">storage</span> </span><span class="hll"> <span class="n">record</span> <span class="o">=</span> <span class="n">storage</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">collection_id</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> </span> <span class="k">return</span> <span class="n">record</span> </pre></div> </div> </div> <div class="section" id="vos-retours"> <h2>Vos retours</h2> <p>N'hésitez pas à nous faire part de vos retours ! Cela vous a donné envie d'essayer ? Vous connaissez un outil similaire ? Y-a-t-il des points qui ne sont pas clairs ? Manque de cas d'utilisation concrets ? Certains aspects mal pensés ? Trop contraignants ? Trop de magie ? Overkill ?</p> <p>Nous prenons tout.</p> <div class="section" id="points-faibles"> <h3>Points faibles</h3> <p>Nous sommes très fiers de ce que nous avons construit, en relativement peu de temps. Et comme nous l'exposions dans <a class="reference external" href="{filename/2015.07.whistler-use-cases.rst}">l'article précédent</a>, il y a du potentiel !</p> <p>Cependant, nous sommes conscients d'un certain nombre de points qui peuvent être vus comme des faiblesses.</p> <ul class="simple"> <li><strong>La documentation d'API</strong> : actuellement, nous n'avons pas de solution pour qu'un projet qui utilise <em>Cliquet</em> puisse intégrer facilement toute <a class="reference external" href="http://cliquet.readthedocs.org/en/latest/api/index.html">la documentation de l'API</a> obtenue.</li> <li><strong>La documentation</strong> : il est très difficile d'organiser la documentation, surtout quand le public visé est aussi bien débutant qu'expérimenté. Nous sommes probablement victimes du «<a class="reference external" href="https://en.wikipedia.org/wiki/Curse_of_knowledge">curse of knowledge</a>».</li> <li><strong>Le protocole</strong> : on sent bien qu'on va devoir versionner le protocole. Au moins pour le désolidariser des versions de <em>Cliquet</em>, si on veut aller au bout de la philosophie et de l'éco-système.</li> <li><strong>Le conservatisme</strong> : Nous aimons la stabilité et la robustesse. Mais surtout nous ne sommes pas tout seuls et devons nous plier aux contraintes de la mise en production ! Cependant, nous avons très envie de faire de l'async avec Python 3 !</li> <li><strong>Publication de versions</strong> : le revers de la médaille de la factorisation. Il arrive qu'on préfère faire évoluer le toolkit (e.g. ajouter une option) pour un point précis d'un projet. En conséquence, on doit souvent releaser les projets en cascade.</li> </ul> </div> <div class="section" id="quelques-questions-courantes"> <h3>Quelques questions courantes</h3> <blockquote> Pourquoi Python ?</blockquote> <p>On prend beaucoup de plaisir à écrire du Python, et le calendrier annoncé initialement était très serré: pas question de tituber avec une technologie mal maitrisée !</p> <p>Et puis, après avoir passé près d'un an sur un projet Node.js, l'équipe avait bien envie de refaire du Python.</p> <blockquote> Pourquoi pas Django ?</blockquote> <p>On y a pensé, surtout parce qu'il y a plusieurs fans de <em>Django REST Framework</em> dans l'équipe.</p> <p>On l'a écarté principalement au profit de la légèreté et la modularité de <em>Pyramid</em>.</p> <blockquote> Pourquoi pas avec un framework asynchrone en Python 3+ ?</blockquote> <p>Pour l'instant nos administrateurs système nous imposent des déploiements en Python 2.7, à notre grand désarroi /o\</p> <p>Pour <em>Reading List</em>, nous <a class="reference external" href="https://github.com/mozilla-services/readinglist/blob/1.7.0/readinglist/__init__.py#L19-L26">avions activé</a> <em>gevent</em>.</p> <p>Puisque l'approche consiste à implémenter un protocole bien déterminé, nous n'excluons pas un jour d'écrire un <em>Cliquet</em> en <em>aiohttp</em> ou <em>Go</em> si cela s'avèrerait pertinent.</p> <blockquote> Pourquoi pas JSON-API ?</blockquote> <p>Comme nous l'expliquions <a class="reference external" href="{filename}/2015.05.retour-apidays.rst">au retour des APIdays</a>, JSON-API est une spécification qui rejoint plusieurs de nos intentions.</p> <p>Quand nous avons commencé le protocole, nous ne connaissions pas JSON-API. Pour l'instant, comme notre proposition est beaucoup plus minimaliste, le rapprochement n'a <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues/254">pas dépassé le stade de la discussion</a>.</p> <blockquote> Est-ce que Cliquet est un framework REST pour Pyramid ?</blockquote> <p>Non.</p> <p>Au delà des classes de resources CRUD de Cliquet, qui implémentent un protocole bien précis, il faut utiliser Cornice ou Pyramid directement.</p> <blockquote> Est-ce que Cliquet est suffisamment générique pour des projets hors Mozilla ?</blockquote> <p>Premièrement, nous faisons en sorte que tout soit contrôlable depuis la configuration <tt class="docutils literal">.ini</tt> pour permettre la dés/activation ou substitution des composants.</p> <p>Si le protocole HTTP/JSON des resources CRUD vous satisfait, alors Cliquet est probablement le plus court chemin pour construire une application qui tient la route.</p> <p>Mais l'utilisation des resources CRUD est facultative, donc Cliquet reste pertinent si les bonnes pratiques en terme de mise en production ou les abstractions fournies vous paraissent valables !</p> <p>Cliquet reste un moyen simple d'aller très vite pour mettre sur pied une application Pyramid/Cornice.</p> <blockquote> Est-ce que les resources JSON supporte les modèles relationnels complexes ?</blockquote> <p>La couche de persistence fournie est très simple, et devrait répondre à la majorité des cas d'utilisation où les données n'ont pas de relations.</p> <p>En revanche, il est tout à fait possible de bénéficier de tous les aspects du protocole en utilisant une classe <tt class="docutils literal">Collection</tt> maison, qui se chargerait elle de manipuler les relations.</p> <p>Le besoin de relations pourrait être un bon prétexte pour implémenter le protocole avec Django REST Framework :)</p> <blockquote> Est-il possible de faire ci ou ça avec Cliquet ?</blockquote> <p>Nous aimerions collecter des besoins pour écrire un ensemble de «recettes/tutoriels». Mais pour ne pas travailler dans le vide, nous aimerions <a class="reference external" href="https://github.com/mozilla-services/cliquet/issues">connaitre vos idées</a> ! (<em>ex. brancher l'authentification Github, changer le format du logging JSON, stocker des données cartographiques, ...</em>)</p> <blockquote> Est-ce que Cliquet peut manipuler des fichiers ?</blockquote> <p><a class="reference external" href="https://github.com/mozilla-services/cliquet/issues/236">Nous l'envisageons</a>, mais pour l'instant nous attendons que le besoin survienne en interne pour se lancer.</p> <p>Si c'est le cas, le protocole utilisé sera <a class="reference external" href="http://remotestorage.io/">Remote Storage</a>, afin notamment de s'intégrer dans l'éco-système grandissant.</p> <blockquote> Est-ce que la fonctionnalité X va être implémentée ?</blockquote> <p><em>Cliquet</em> est déjà bien garni. Plutôt qu'implémenter la fonctionnalité X, il y a de grandes chances que nous agissions pour s'assurer que les abstractions et les mécanismes d'extension fournis permettent de l'implémenter sous forme d'extension.</p> </div> </div> Service de nuages : Perspectives pour l'été2015-07-07T00:00:00+02:002015-07-07T00:00:00+02:00tag:blog.notmyidea.org,2015-07-07:/service-de-nuages-perspectives-pour-lete-fr.html<p class="first last">Le travail en cours et les fonctionnalités à venir pour les prochains mois.</p> <p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p> <p>Mozilla a pour coutume d'organiser régulièrement des semaines de travail où tous les employés sont réunis physiquement. Pour cette dernière édition, nous avons pu retrouver nos collègues du monde entier à <a class="reference external" href="http://www.openstreetmap.org/node/268148288#map=4/50.12/-122.95">Whistler, en Colombie Britannique au Canada</a> !</p> <img alt="«All Hands» talk about Lego, by &#64;davidcrob - CC0" class="align-center" src="{filename}/images/whistler-talks.jpg" /> <p>Ce fût l'occasion pour notre équipe de se retrouver, et surtout de partager notre vision et nos idées dans le domaine du stockage, afin de collecter des cas d'utilisation pour notre solution <a class="reference external" href="https://kinto.readthedocs.org">Kinto</a>.</p> <p>Dans cet article, nous passons en revue les pistes que nous avons pour les prochains mois.</p> <div class="section" id="ateliers-et-promotion"> <h2>Ateliers et promotion</h2> <p>Nicolas a présenté <a class="reference external" href="https://github.com/mozilla-services/kinto.js">Kinto.js</a> dans un atelier dédié, avec comme support de présentation le <a class="reference external" href="http://kintojs.readthedocs.org/en/latest/tutorial/">tutorial d'introduction</a>.</p> <p>L'application résultante, pourtant toute simple, permet d'appréhender les concepts de synchronisation de Kinto. Le tout sans installation prélable, puisque Rémy a mis en place un <a class="reference external" href="https://kinto.dev.mozaws.net/v1/">serveur de dev effacé tous les jours</a>.</p> <p>Nous avions mis un point d'honneur à faire du Vanilla.JS, déjà pour éviter les combats de clochers autour des frameworks, mais aussi pour mettre en évidence qu'avec HTML5 et ES6, on n'était plus aussi démunis qu'il y a quelques années.</p> <p>Ce petit atelier nous a permis de nous rendre compte qu'on avait encore de grosses lacunes en terme de documentation, surtout en ce qui concerne l'éco-système et la vision globale des projets (Kinto, Kinto.js, Cliquet, ...). Nous allons donc faire de notre mieux pour combler ce manque.</p> <img alt="Kinto.js workshop - CC0" class="align-center" src="{filename}/images/whistler-workshop.jpg" /> </div> <div class="section" id="mozilla-payments"> <h2>Mozilla Payments</h2> <p>Comme <a class="reference external" href="http://www.servicedenuages.fr/la-gestion-des-permissions">décrit précédemment</a>, nous avons mis en place un système de permissions pour répondre aux besoins de suivi des paiements et abonnements.</p> <p>Pour ce projet, Kinto sera utilisé depuis une application Django, via un client Python.</p> <p>Maintenant que les développements ont été livrés, il faut transformer l'essai, réussir l'intégration, l'hébergement et la montée en puissance. La solution doit être livrée à la fin de l'année.</p> <div class="section" id="a-venir"> <h3>À venir</h3> <p>Nous aimerions en profiter pour implémenter une fonctionnalité qui nous tient à coeur : la construction de la liste des enregistrements accessibles en lecture sur une collection partagée.</p> <img alt="Whistler Alta Lake - CC0" class="align-center" src="{filename}/images/whistler-lake.jpg" /> </div> </div> <div class="section" id="firefox-os-et-stockage"> <h2>Firefox OS et stockage</h2> <p>Nous avons eu beaucoup d'échanges avec l'équipe de Firefox OS, avec qui nous avions déjà eu l'occasion de collaborer, pour le <a class="reference external" href="https://github.com/mozilla-services/msisdn-gateway">serveur d'identification BrowserID par SMS</a> et pour <a class="reference external" href="https://github.com/mozilla-services/loop-server">Firefox Hello</a>.</p> <div class="section" id="in-app-sync"> <h3>In-App sync</h3> <p>Kinto, la solution simple promue pour la synchronisation de données dans les applications Firefox OS ? La classe ! C'est ce qu'on avait en tête depuis longtemps, déjà à l'époque avec <a class="reference external" href="http://daybed.readthedocs.org/">Daybed</a>. Voici donc une belle opportunité à saisir !</p> <p>Il va falloir expliciter les limitations et hypothèses simplificatrices de notre solution, surtout en termes de gestion de la concurrence. Nous sommes persuadés que ça colle avec la plupart des besoins, mais il ne faudrait pas décevoir :)</p> <p>Le fait que <a class="reference external" href="https://github.com/daleharvey">Dale</a>, un des auteurs de <a class="reference external" href="http://pouchdb.com/">PouchDB</a> et <a class="reference external" href="https://github.com/michielbdejong">Michiel de Jong</a>, un des auteurs de <a class="reference external" href="http://remotestorage.io/">Remote Storage</a>, nous aient encouragés sur nos premiers pas nous a bien motivé !</p> </div> <div class="section" id="cut-the-rope"> <h3>Cut the Rope</h3> <p>Kinto devrait être mis à profit pour synchroniser les paramètres et les scores du <a class="reference external" href="http://mozilla.cuttherope.net/">jeu</a>. Un premier exercice et une première vitrine sympas !</p> </div> <div class="section" id="syncto"> <h3>« SyncTo »</h3> <p><a class="reference external" href="https://docs.services.mozilla.com/storage/apis-1.5.html">Firefox Sync</a> est la solution qui permet de synchroniser les données de Firefox (favoris, extensions, historique, complétion des formulaires, mots de passe, ...) entre plusieurs périphériques, de manière chiffrée.</p> <p>L'implémentation du client en JavaScript est relativement complexe et date un peu maintenant. Le code existant n'est pas vraiment portable dans <em>Firefox OS</em> et les tentatives de réécriture n'ont pas abouti.</p> <p>Nous souhaitons implémenter un pont entre <em>Kinto</em> et <em>Firefox Sync</em>, de manière à pouvoir utiliser le client <em>Kinto.js</em>, plus simple et plus moderne, pour récupérer les contenus et les stocker dans IndexedDB. Le delta à implémenter côté serveur est faible car nous nous étions inspirés du protocole déjà éprouvé de Sync. Côté client, il s'agira surtout de câbler l'authentification BrowserId et la Crypto.</p> <p>Alexis a sauté sur l'occasion pour commencer l'écriture d'<a class="reference external" href="https://github.com/mozilla-services/syncclient">un client python pour Firefox Sync</a>, qui servira de brique de base pour l'écriture du service.</p> </div> <div class="section" id="cloud-storage"> <h3>Cloud Storage</h3> <p>Eden Chuang et Sean Lee ont présenté les avancées sur l'intégration de services de stockages distants (<em>DropBox, Baidu Yun</em>) dans <em>Firefox OS</em>. Actuellement, leur preuve de concept repose sur <a class="reference external" href="https://fr.wikipedia.org/wiki/Filesystem_in_Userspace">FUSE</a>.</p> <p>Nous avons évidemment en tête d'introduire la notion de fichiers attachés dans <em>Kinto</em>, en implémentant la specification <a class="reference external" href="https://tools.ietf.org/html/draft-dejong-remotestorage-05">*Remote Storage*</a>, mais pour l'instant les cas d'utilisations ne se sont pas encore présentés officiellement.</p> </div> <div class="section" id="id2"> <h3>À venir</h3> <p>Nous serons probablement amenés à introduire la gestion de la concurrence dans le client JS, en complément de ce qui a été fait sur le serveur, pour permettre les écritures simultanées et synchronisation en tâche de fond.</p> <p>Nous sommes par ailleurs perpétuellement preneurs de vos retours — et bien entendu de vos contributions — tant sur le code <a class="reference external" href="https://github.com/mozilla-services/kinto/">serveur</a> que <a class="reference external" href="https://github.com/mozilla-services/kinto.js/">client</a> !</p> <img alt="Firefox OS Cloud Storage Presentation - CC0" class="align-center" src="{filename}/images/whistler-cloud-storage.jpg" /> </div> </div> <div class="section" id="contenus-applicatifs-de-firefox"> <h2>Contenus applicatifs de Firefox</h2> <p>Aujourd'hui Firefox a un cycle de release de six semaines. Un des objectifs consiste à désolidariser certains contenus applicatifs de ces cycles relativement longs (ex. <em>règles de securité, dictionnaires, traductions, ...</em>) <a class="footnote-reference" href="#id4" id="id3">[1]</a>.</p> <p>Il s'agit de données JSON et binaire qui doivent être versionnées et synchronisées par les navigateurs (<em>lecture seule</em>).</p> <p>Il y a plusieurs outils officiels qui existent pour gérer ça (<em>Balrog</em>, <em>Shavar</em>, ...), et pour l'instant, aucun choix n'a été fait. Mais lors des conversations avec l'équipe en charge du projet, ce fût vraiment motivant de voir que même pour ce genre de besoins internes, <em>Kinto</em> est tout aussi pertinent !</p> <table class="docutils footnote" frame="void" id="id4" rules="none"> <colgroup><col class="label" /><col /></colgroup> <tbody valign="top"> <tr><td class="label"><a class="fn-backref" href="#id3">[1]</a></td><td>La bonne nouvelle c'est que toutes les fonctionnalités <em>third-party</em> qui ont été intégrées récemment vont redevenir des <em>add-ons</em> \o/.</td></tr> </tbody> </table> <img alt="Landscape - CC0" class="align-center" src="{filename}/images/whistler-landscape.jpg" /> </div> <div class="section" id="awesome-bar"> <h2>Awesome bar</h2> <p>L'équipe <em>Firefox Labs</em>, le laboratoire qui élève des pandas roux en éprouvette, serait vraiment intéressé par notre solution, notamment pour abreuver en données un prototype pour améliorer <em>Awesome bar</em>, qui fusionnerait URL, historique et recherche.</p> <p>Nous ne pouvons pas en dire beaucoup plus pour l'instant, mais les fonctionnalités de collections d'enregistrements partagées entre utilisateurs de <em>Kinto</em> correspondent parfaitement à ce qui est envisagé pour le futur du navigateur :)</p> <div class="section" id="id5"> <h3>À venir</h3> <p>Nous serons donc probablement amenés, avant de la fin de l'année, à introduire des fonctionnalités d'indexation et de recherche <em>full-text</em> (comprendre <em>ElasticSearch</em>). Cela rejoint nos plans précédents, puisque c'est quelque chose que nous avions dans <em>Daybed</em>, et qui figurait sur notre feuille de route !</p> <img alt="Firefox Labs Meeting - CC0" class="align-center" src="{filename}/images/whistler-labs.jpg" /> </div> </div> <div class="section" id="browser-html"> <h2>Browser.html</h2> <p>L'équipe <em>Recherche</em> explore les notions de plateforme, et travaille notamment sur l'implémentation d'un navigateur en JS/HTML avec <em>React</em>: <a class="reference external" href="https://github.com/mozilla/browser.html">browser.html</a></p> <p><em>Kinto</em> correspond parfaitement aux attentes de l'équipe pour synchroniser les données associées à un utilisateur.</p> <p>Il pourrait s'agir de données de navigation (comme Sync), mais aussi de collections d'enregistrements diverses, comme par exemple les préférences du navigateur ou un équivalent à <em>Alexa.com Top 500</em> pour fournir la complétion d'URL sans interroger le moteur de recherche.</p> <p>L'exercice pourrait être poussé jusqu'à la synchronisation d'états <em>React</em> entre périphériques (par exemple pour les onglets).</p> <div class="section" id="id7"> <h3>À venir</h3> <p>Si <em>browser.html</em> doit stocker des données de navigation, il faudra ajouter des fonctionnalités de chiffrement sur le client JS. Ça tombe bien, c'est un sujet passionant, et <a class="reference external" href="http://www.w3.org/TR/WebCryptoAPI/">il y a plusieurs standards</a> !</p> <p>Pour éviter d'interroger le serveur à intervalle régulier afin de synchroniser les changements, l'introduction des <a class="reference external" href="https://w3c.github.io/push-api/">*push notifications*</a> semble assez naturelle. Il s'agirait alors de la dernière pierre qui manque à l'édifice pour obtenir un «<em>Mobile/Web backend as a service</em>» complet.</p> <img alt="Roadmap - CC0" class="align-center" src="{filename}/images/whistler-roadmap.jpg" /> </div> </div> <div class="section" id="conclusion"> <h2>Conclusion</h2> <p>Nous sommes dans une situation idéale, puisque ce que nous avions imaginé sur <a class="reference external" href="https://github.com/mozilla-services/kinto/wiki/Roadmap">notre feuille de route</a> correspond à ce qui nous est demandé par les différentes équipes.</p> <p>L'enjeu consiste maintenant à se coordonner avec tout le monde, ne pas décevoir, tenir la charge, continuer à améliorer et à faire la promotion du produit, se concentrer sur les prochaines étapes et embarquer quelques contributeurs à nos cotés pour construire une solution libre, générique, simple et auto-hébergeable pour le stockage de données sur le Web :)</p> <img alt="Friday Night Party - CC0" class="align-center" src="{filename}/images/whistler-top-roof.jpg" /> </div> Service de nuages : Stocker et interroger les permissions avec Kinto2015-05-26T00:00:00+02:002015-05-26T00:00:00+02:00tag:blog.notmyidea.org,2015-05-26:/service-de-nuages-stocker-et-interroger-les-permissions-avec-kinto-fr.html<p class="first last">Comment faire pour stocker et interroger la base de données au sujet des permissions avec Kinto ?</p> <p><em>Cet article est repris depuis le blog « Service de Nuages » de mon équipe à Mozilla</em></p> <p><strong>tl;dr: On a maintenant un super système de permission mais comment faire pour stocker et interroger ces permissions de manière efficace ?</strong></p> <div class="section" id="la-problematique"> <h2>La problématique</h2> <p>Maintenant que nous avons défini un modèle de gestion des permissions sur les objets qui nous satisfait, le problème est de stocker ces permissions de manière efficace afin de pouvoir autoriser ou interdire l'accès à un objet pour la personne qui fait la requête.</p> <p>Chaque requête sur notre API va générer une ou plusieurs demandes d'accès, il faut donc que la réponse soit très rapide sous peine d'impacter la vélocité du service.</p> </div> <div class="section" id="obtenir-la-liste-des-principals-d-un-utilisateur"> <h2>Obtenir la liste des &quot;principals&quot; d'un utilisateur</h2> <p>Les <em>principals</em> de l'utilisateur correspondent à son <tt class="docutils literal">user_id</tt> ainsi qu'à la liste des identifiants des groupes dans lesquels il a été ajouté.</p> <p>Pour éviter de recalculer les <em>principals</em> de l'utilisateur à chaque requête, le mieux reste de maintenir une liste des <em>principals</em> par utilisateur.</p> <p>Ainsi lorsqu'on ajoute un utilisateur à un groupe, il faut bien penser à ajouter le groupe à la liste des <em>principals</em> de l'utilisateur.</p> <p>Ça se complexifie lorsqu'on ajoute un groupe à un groupe.</p> <p>Dans un premier temps interdire l'ajout d'un groupe à un groupe est une limitation qu'on est prêts à accepter pour simplifier le modèle.</p> <p>L'avantage de maintenir la liste des <em>principals</em> d'un utilisateur lors de la modification de cette liste c'est qu'elle est déjà construite lors des lectures, qui sont dans notre cas plus fréquentes que les écritures.</p> <p>Cela nécessite de donner un identifiant unique aux groupes pour tous les <em>buckets</em>.</p> <p>Nous proposons de de les nommer avec leur URI: <tt class="docutils literal">/buckets/blog/groups/moderators</tt></p> </div> <div class="section" id="obtenir-la-liste-des-principals-d-un-ace"> <h2>Obtenir la liste des &quot;principals&quot; d'un ACE</h2> <blockquote> Rappel, un &quot;ACE&quot; est un <em>Access Control Entry</em>, un des éléments d'une ACL (e.g. <em>modifier un enregistrement</em>).</blockquote> <p>Avec le <a class="reference external" href="{filename}/2015.05.cliquet-permissions.rst">système de permissions choisi</a>, les permissions d'un objet héritent de celle de l'objet parent.</p> <p>Par exemple, avoir le droit d'écriture sur un <em>bucket</em> permet la création des permissions et la modification de tous ses records.</p> <p>Ce qui veut dire que pour obtenir la liste complète des <em>principals</em> ayant une permission sur un objet, il faut regarder à plusieurs endroits.</p> <p>Rémy a <a class="reference external" href="https://gist.github.com/Natim/77c8f61c1d42e476cef8#file-permission-py-L9-L52">décrit dans un gist la liste d'héritage de chaque permission</a>.</p> <p>Prenons l'exemple de l'ajout d'un record dans une collection.</p> <p>Le droit <tt class="docutils literal">records:create</tt> est obtenu si l'on a l'un des droits suivants:</p> <ul class="simple"> <li><tt class="docutils literal">bucket:write</tt></li> <li><tt class="docutils literal">collection:write</tt></li> <li><tt class="docutils literal">records:create</tt></li> </ul> <p>Notre première idée était de stocker les permissions sur chaque objet et de maintenir la liste exhaustive des permissions lors d'une modification d'ACL. Cependant cela nécessitait de construire cette liste lors de l'ajout d'un objet et de mettre à jour tout l'arbre lors de sa suppression. (<em>Je vous laisse imaginer le nombre d'opérations nécessaires pour ajouter un administrateur sur un *bucket</em> contenant 1000 collections avec 100000 records chacune.*)</p> <p>La solution que nous avons désormais adoptée consiste à stocker les <em>principals</em> de chaque <em>ACE</em> (<em>qui</em> a le droit de faire telle action sur l'objet), et de faire l'union des <em>ACE</em> hérités, afin de les croiser avec les <em>principals</em> de l'utilisateur :</p> <blockquote> (ACE(object, permission) ∪ inherited_ACE) ∩ PRINCIPALS(user)</blockquote> <p>Par exemple l'ACE: <tt class="docutils literal">/buckets/blog/collections/article:records:create</tt> hérite de l'ACE <tt class="docutils literal">/buckets/blog/collections/article:write</tt> et de <tt class="docutils literal">/buckets/blog:write</tt> :</p> <blockquote> (ACE(/buckets/blog/collections/article:records:create) ∪ ACE(/buckets/blog/collections/article:write) ∪ ACE(/buckets/blog:write)) ∩ PRINCIPALS('fxa:alexis')</blockquote> </div> <div class="section" id="recuperer-les-donnees-de-l-utilisateur"> <h2>Récupérer les données de l'utilisateur</h2> <p>La situation se corse lorsqu'on souhaite limiter la liste des <em>records</em> d'une collection à ceux accessibles pour l'utilisateur, car on doit faire cette intersection pour tous les <em>records</em>.</p> <p>Une première solution est de regarder si l'utilisateur est mentionné dans les <em>ACL*s du *bucket</em> ou de la <em>collection</em>:</p> <p>Ensuite, si ce n'est pas le cas, alors on filtre les <em>records</em> pour lesquels les <em>principals</em> correspondent à ceux de l'utilisateur.</p> <div class="highlight"><pre><span></span><span class="n">principals</span> <span class="o">=</span> <span class="n">get_user_principals</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span> <span class="n">can_read_all</span> <span class="o">=</span> <span class="n">has_read_perms</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span> <span class="n">principals</span><span class="p">)</span> <span class="k">if</span> <span class="n">can_read_all</span><span class="p">:</span> <span class="n">records</span> <span class="o">=</span> <span class="n">get_all_records</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span> <span class="n">filters</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">])</span> <span class="k">else</span><span class="p">:</span> <span class="n">records</span> <span class="o">=</span> <span class="n">filter_read_records</span><span class="p">(</span><span class="n">bucket_id</span><span class="p">,</span> <span class="n">collection_id</span><span class="p">,</span> <span class="n">principals</span><span class="o">=</span><span class="n">principals</span><span class="p">,</span> <span class="n">filters</span><span class="o">=</span><span class="p">[</span><span class="o">...</span><span class="p">])</span> </pre></div> <p>Il faudra faire quelque chose de similaire pour la suppression multiple, lorsqu'un utilisateur souhaitera supprimer des enregistrements sur lesquels il a les droits de lecture mais pas d'écriture.</p> </div> <div class="section" id="le-modele-de-donnees"> <h2>Le modèle de données</h2> <p>Pour avoir une idée des requêtes dans un backend SQL, voyons un peu ce que donnerait le modèle de données.</p> <div class="section" id="le-format-des-id"> <h3>Le format des ID</h3> <p>Utiliser des URI comme identifiant des objets présente de nombreux avantages (lisibilité, unicité, cohérence avec les URLs)</p> <ul class="simple"> <li>bucket: <tt class="docutils literal">/buckets/blog</tt></li> <li>groupe: <tt class="docutils literal">/buckets/blog/group/moderators</tt></li> <li>collection: <tt class="docutils literal">/buckets/blog/collections/articles</tt></li> <li>record: <tt class="docutils literal"><span class="pre">/buckets/blog/collections/articles/records/02f3f76f-7059-4ae4-888f-2ac9824e9200</span></tt></li> </ul> </div> <div class="section" id="les-tables"> <h3>Les tables</h3> <p>Pour le stockage des principals et des permissions:</p> <div class="highlight"><pre><span></span><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">user</span><span class="p">(</span><span class="n">id</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">principals</span> <span class="nb">TEXT</span><span class="p">[]);</span> <span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">perms</span><span class="p">(</span><span class="n">ace</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">principals</span> <span class="nb">TEXT</span><span class="p">[]);</span> </pre></div> <p>La table <em>perms</em> va associer des <em>principals</em> à chaque <em>ACE</em> (e.g.``/buckets/blog:write``).</p> <p>Pour le stockage des données:</p> <div class="highlight"><pre><span></span><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">object</span><span class="p">(</span><span class="n">id</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="k">type</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">parent_id</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="k">data</span> <span class="n">JSONB</span><span class="p">,</span> <span class="n">write_principals</span> <span class="nb">TEXT</span><span class="p">[],</span> <span class="n">read_principals</span> <span class="nb">TEXT</span><span class="p">[]);</span> </pre></div> <p>La colonne <em>parent_id</em> permet de savoir à qui appartient l'objet (e.g. groupe d'un <em>bucket</em>, collection d'un <em>bucket</em>, <em>record</em> d'une collection, ...).</p> </div> <div class="section" id="exemple-d-utilisateur"> <h3>Exemple d'utilisateur</h3> <div class="highlight"><pre><span></span><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">user</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">&#39;fxa:alexis&#39;</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">);</span> <span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">user</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="n">principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span><span class="s1">&#39;fxa:natim&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;/buckets/blog/groups/moderators&quot;}&#39;</span><span class="p">);</span> </pre></div> </div> <div class="section" id="exemple-d-objets"> <h3>Exemple d'objets</h3> <div class="section" id="bucket"> <h4>Bucket</h4> <div class="highlight"><pre><span></span><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">object</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="n">parent_id</span><span class="p">,</span> <span class="k">data</span><span class="p">,</span> <span class="n">read_principals</span><span class="p">,</span> <span class="n">write_principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span> <span class="s1">&#39;/buckets/blog&#39;</span><span class="p">,</span> <span class="s1">&#39;bucket&#39;</span><span class="p">,</span> <span class="k">NULL</span><span class="p">,</span> <span class="s1">&#39;{&quot;name&quot;: &quot;blog&quot;}&#39;</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;fxa:alexis&quot;}&#39;</span><span class="p">);</span> </pre></div> </div> <div class="section" id="group"> <h4>Group</h4> <div class="highlight"><pre><span></span><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">object</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="n">parent_id</span><span class="p">,</span> <span class="k">data</span><span class="p">,</span> <span class="n">read_principals</span><span class="p">,</span> <span class="n">write_principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span> <span class="s1">&#39;/buckets/blog/groups/moderators&#39;</span><span class="p">,</span> <span class="s1">&#39;group&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;name&quot;: &quot;moderators&quot;, &quot;members&quot;: [&#39;</span><span class="n">fxa</span><span class="p">:</span><span class="n">natim</span><span class="s1">&#39;]}&#39;</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">);</span> </pre></div> <p>Ce groupe peut être gére par <tt class="docutils literal">fxa:alexis</tt> puisqu'il a la permission <tt class="docutils literal">write</tt> dans le <em>bucket</em> parent.</p> </div> <div class="section" id="collection"> <h4>Collection</h4> <div class="highlight"><pre><span></span><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">object</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="n">parent_id</span><span class="p">,</span> <span class="k">data</span><span class="p">,</span> <span class="n">read_principals</span><span class="p">,</span> <span class="n">write_principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span> <span class="s1">&#39;/buckets/blog/collections/articles&#39;</span><span class="p">,</span> <span class="s1">&#39;collection&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;name&quot;: &quot;article&quot;}&#39;</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span> <span class="s1">&#39;{&quot;system.Everyone&quot;}&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;/buckets/blog/groups/moderators&quot;}&#39;</span><span class="p">);</span> </pre></div> <p>Cette collection d'articles peut être lue par tout le monde, et gérée par les membres du groupe <tt class="docutils literal">moderators</tt>, ainsi que <tt class="docutils literal">fxa:alexis</tt>, via le <em>bucket</em>.</p> </div> <div class="section" id="records"> <h4>Records</h4> <div class="highlight"><pre><span></span><span class="k">INSERT</span> <span class="k">INTO</span> <span class="k">object</span> <span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">type</span><span class="p">,</span> <span class="n">parent_id</span><span class="p">,</span> <span class="k">data</span><span class="p">,</span> <span class="n">read_principals</span><span class="p">,</span> <span class="n">write_principals</span><span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span> <span class="s1">&#39;/buckets/blog/collections/articles/records/02f3f76f-7059-4ae4-888f-2ac9824e9200&#39;</span><span class="p">,</span> <span class="s1">&#39;record&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog/collections/articles&#39;</span><span class="p">,</span> <span class="s1">&#39;{&quot;name&quot;: &quot;02f3f76f-7059-4ae4-888f-2ac9824e9200&quot;,</span> <span class="s1"> &quot;title&quot;: &quot;Stocker les permissions&quot;, ...}&#39;</span><span class="p">::</span><span class="n">JSONB</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">,</span> <span class="s1">&#39;{}&#39;</span><span class="p">);</span> </pre></div> </div> </div> <div class="section" id="interroger-les-permissions"> <h3>Interroger les permissions</h3> <div class="section" id="id1"> <h4>Obtenir la liste des &quot;principals&quot; d'un ACE</h4> <p>Comme vu plus haut, pour vérifier une permission, on fait l'union des <em>principals</em> requis par les objets hérités, et on teste leur intersection avec ceux de l'utilisateur:</p> <div class="highlight"><pre><span></span><span class="k">WITH</span> <span class="n">required_principals</span> <span class="k">AS</span> <span class="p">(</span> <span class="k">SELECT</span> <span class="k">unnest</span><span class="p">(</span><span class="n">principals</span><span class="p">)</span> <span class="k">AS</span> <span class="n">p</span> <span class="k">FROM</span> <span class="n">perms</span> <span class="k">WHERE</span> <span class="n">ace</span> <span class="k">IN</span> <span class="p">(</span> <span class="s1">&#39;/buckets/blog:write&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog:read&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog/collections/article:write&#39;</span><span class="p">,</span> <span class="s1">&#39;/buckets/blog/collections/article:read&#39;</span><span class="p">)</span> <span class="p">),</span> <span class="n">user_principals</span> <span class="k">AS</span> <span class="p">(</span> <span class="k">SELECT</span> <span class="k">unnest</span><span class="p">(</span><span class="n">principals</span><span class="p">)</span> <span class="k">FROM</span> <span class="k">user</span> <span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="s1">&#39;fxa:natim&#39;</span> <span class="p">)</span> <span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">user_principals</span> <span class="n">a</span> <span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">required_principals</span> <span class="n">b</span> <span class="k">ON</span> <span class="n">a</span><span class="p">.</span><span class="n">p</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="n">p</span><span class="p">;</span> </pre></div> </div> <div class="section" id="filtrer-les-objets-en-fonction-des-permissions"> <h4>Filtrer les objets en fonction des permissions</h4> <p>Pour filtrer les objets, on fait une simple intersection de liste (<em>merci PostgreSQL</em>):</p> <div class="highlight"><pre><span></span><span class="k">SELECT</span> <span class="k">data</span> <span class="k">FROM</span> <span class="k">object</span> <span class="n">o</span><span class="p">,</span> <span class="k">user</span> <span class="n">u</span> <span class="k">WHERE</span> <span class="n">o</span><span class="p">.</span><span class="k">type</span> <span class="o">=</span> <span class="s1">&#39;record&#39;</span> <span class="k">AND</span> <span class="n">o</span><span class="p">.</span><span class="n">parent_id</span> <span class="o">=</span> <span class="s1">&#39;/buckets/blog/collections/article&#39;</span> <span class="k">AND</span> <span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">read_principals</span> <span class="o">&amp;&amp;</span> <span class="n">u</span><span class="p">.</span><span class="n">principals</span> <span class="k">OR</span> <span class="n">o</span><span class="p">.</span><span class="n">write_principals</span> <span class="o">&amp;&amp;</span> <span class="n">u</span><span class="p">.</span><span class="n">principals</span><span class="p">)</span> <span class="k">AND</span> <span class="n">u</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="s1">&#39;fxa:natim&#39;</span><span class="p">;</span> </pre></div> <p>Les listes s'indexent bien, notamment grâce aux <a class="reference external" href="http://www.postgresql.org/docs/current/static/indexes-types.html">index GIN</a>.</p> </div> </div> <div class="section" id="avec-redis"> <h3>Avec Redis</h3> <p><em>Redis</em> présente plusieurs avantages pour ce genre de problématiques. Notamment, il gère les <em>set</em> nativement (listes de valeurs uniques), ainsi que les opérations d'intersection et d'union.</p> <p>Avec <em>Redis</em> on peut écrire l'obtention des <em>principals</em> pour un <em>ACE</em> comme cela :</p> <div class="highlight"><pre><span></span>SUNIONSTORE temp_perm:/buckets/blog/collections/articles:write permission:/buckets/blog:write permission:/buckets/blog/collections/articles:write SINTER temp_perm:/buckets/blog/collections/articles:write principals:fxa:alexis </pre></div> <ul class="simple"> <li><tt class="docutils literal">SUNIONSTORE</tt> permet de créer un set contenant les éléments de l'union de tous les set suivants. Dans notre cas on le nomme <tt class="docutils literal"><span class="pre">temp_perm:/buckets/blog/collections/articles:write</span></tt> et il contient l'union des sets d'ACLs suivants: - <tt class="docutils literal"><span class="pre">permission:/buckets/blog:write</span></tt> - <tt class="docutils literal"><span class="pre">permission:/buckets/blog/collections/articles:write</span></tt></li> <li><tt class="docutils literal">SINTER</tt> retourne l'intersection de tous les sets passés en paramètres dans notre cas : - <tt class="docutils literal"><span class="pre">temp_perm:/buckets/blog/collections/articles:write</span></tt> - <tt class="docutils literal">principals:fxa:alexis</tt></li> </ul> <p>Plus d'informations sur : - <a class="reference external" href="http://redis.io/commands/sinter">http://redis.io/commands/sinter</a> - <a class="reference external" href="http://redis.io/commands/sunionstore">http://redis.io/commands/sunionstore</a></p> <p>Si le set résultant de la commande <tt class="docutils literal">SINTER</tt> n'est pas vide, alors l'utilisateur possède la permission.</p> <p>On peut ensuite supprimer la clé temporaire <tt class="docutils literal">temp_perm</tt>.</p> <p>En utilisant <tt class="docutils literal">MULTI</tt> on peut <a class="reference external" href="https://gist.github.com/Natim/77c8f61c1d42e476cef8#file-permission-py-L117-L124">même faire tout cela au sein d'une transaction</a> et garantir ainsi l'intégrité de la requête.</p> </div> </div> <div class="section" id="conclusion"> <h2>Conclusion</h2> <p>La solution a l'air simple mais nous a demandé beaucoup de réflexion en passant par plusieurs propositions.</p> <p>L'idée finale est d'avoir :</p> <ul class="simple"> <li>Un backend spécifique permettant de stocker les <em>principals</em> des utilisateurs et des <em>ACE</em> (e.g. avec les sets Redis) ;</li> <li>La liste des principals read et write sur la table des objets.</li> </ul> <p>C'est dommage d'avoir le concept de permissions à deux endroits, mais cela permet de connaître rapidement la permission d'un utilisateur sur un objet et également de pouvoir récupérer tous les objets d'une collection pour un utilisateur si celui-ci n'a pas accès à tous les records de la collection, ou toutes les collections du bucket.</p> </div> Les problèmes de PGP2015-05-25T00:00:00+02:002015-05-25T00:00:00+02:00tag:blog.notmyidea.org,2015-05-25:/les-problemes-de-pgp.html<blockquote> <p>Flip a bit in the communication between sender and recipient and they will experience decryption or verification errors. How high are the chances they will start to exchange the data in the clear rather than trying to hunt down the man in the middle?</p> <p>-- <a href="http://secushare.org/PGP">http://secushare.org/PGP</a></p> </blockquote> <p>Une fois …</p><blockquote> <p>Flip a bit in the communication between sender and recipient and they will experience decryption or verification errors. How high are the chances they will start to exchange the data in the clear rather than trying to hunt down the man in the middle?</p> <p>-- <a href="http://secushare.org/PGP">http://secushare.org/PGP</a></p> </blockquote> <p>Une fois passé l'euphorie du "il faut utiliser PGP pour l'ensemble de nos communications", j'ai réalisé lors de discussions que PGP avait plusieurs problèmes, parmi ceux-ci:</p> <ul> <li>Les <em>meta données</em> (y compris le champ "sujet" de la conversation) sont quand même échangées en clair (il est possible de savoir qu'un message à été échangé entre telle et telle personne, a telle date);</li> <li>PGP se base sur un protocole de communication qui est lui non chiffré, et il est donc facile de soit se tromper, soit dégrader le mode de conversation vers une méthode non chiffrée;</li> <li>Il est facile de connaître votre réseau social avec PGP, puisque tout le principe est de signer les clés des personnes dont vous validez l'identité;</li> <li>En cas de fuite de votre clé privée, tous les messages que vous avez chiffrés avec elle sont compromis. On dit que PGP ne fournit pas de <em>forward secrecy</em>;</li> <li>La découverte de la clé de pairs se passe souvent <em>en clair</em>, sans utiliser une connexion "sécurisée" (HTTPS). Tout le monde peut donc voir ces échanges et savoir de qui vous cherchez la clé;</li> <li>Les discussions de groupes sont très difficiles: il faut chiffrer pour chacun des destinataires (ou que ceux-ci partagent une paire de clés).</li> </ul> <p>Je suis en train de creuser à propos les alternatives à PGP, par exemple <a href="https://pond.imperialviolet.org/">Pond</a>, qui lui ne construit pas par dessus un standard déjà établi, et donc n'hérite pas de ses défauts (mais pas non plus de son réseau déjà établi).</p> <p>En attendant, quelques bonnes pratiques sur PGP ;)</p> <h2>Bonnes pratiques</h2> <p>Il est en fait assez facile d'utiliser PGP de travers. Riseup à fait <a href="https://help.riseup.net/en/security/message-security/openpgp/best-practices">un excellent guide</a> qui explique comment configurer son installation correctement.</p> <ul> <li>J'en ai déjà parlé, mais il faut absolument choisir des phrases de passes suffisamment longues. Pas facile de les retenir, mais indispensable. Vous pouvez aussi avoir un document chiffré avec une clé que vous ne mettez jamais en ligne, qui contiens ces phrases de passe, au cas ou vous les oubliez.</li> <li>Générez des clés RSA de 4096 bits, en utilisant sha512;</li> <li>Il faut utiliser une date d'expiration de nos clés suffisamment proche (2 ans). Il est possible de repousser cette date si nécessaire, par la suite.</li> </ul> <p>Parmi les choses les plus frappantes que j'ai rencontrées:</p> <ul> <li>Utiliser le <em>flag</em> –hidden-recipient avec PGP pour ne pas dévoiler qui est le destinataire du message;</li> <li>Ne pas envoyer les messages de brouillons sur votre serveur, ils le seraient en clair !;</li> <li>Utilisez HPKS pour communiquer avec les serveurs de clés, sinon tout le trafic est en clair.</li> </ul> <p>Le <a href="https://bitmask.net/">projet Bitmask</a> vise lui à rendre les outils de chiffrement d'échanges de messages et de VPN simples à utiliser, encore quelque chose à regarder.</p> <p>Enfin bref, y'a du taf.</p>