Zanzibar demystifie : comment Google repond a 'cet utilisateur a-t-il le droit ?'
18 juillet 2026 · 14 min read · Read in English
Sommaire
Toute application jamais ecrite doit repondre a une question, encore et encore : cet utilisateur peut-il faire cette action sur cette ressource ? Alice peut-elle lire ce document. Bob peut-il supprimer ce commentaire. Ce service peut-il appeler cet endpoint. On dirait la ligne de code la plus ennuyeuse du batiment, un simple if.
Puis le produit grandit. Le document vit dans un dossier, et l'acces au dossier doit se propager. Le document est partage avec une equipe, l'equipe a des sous-equipes, et les gens arrivent et partent. Quelqu'un au support doit repondre a "pourquoi Bob voit-il ce fichier ?" et personne ne le peut, car la reponse est eparpillee dans six services ayant chacun son propre if. Puis un utilisateur est retire d'un groupe et, pendant les trente secondes suivantes, un cache perime quelque part repond encore oui. Ce n'est plus un if ennuyeux. C'est un probleme de systemes distribues deguise en booleen.
Google a rencontre cela a une echelle que personne d'autre n'avait : une seule question d'autorisation que Docs, Drive, Photos, Calendar, YouTube et Cloud devaient tous poser, correctement, des milliards de fois par jour, en moins de dix millisecondes, partout sur Terre. Leur reponse fut Zanzibar, decrite dans un article de 2019 qui est discretement devenu le design d'autorisation le plus influent de la decennie. On parcourt ici ce que c'est vraiment, pourquoi chaque partie difficile est difficile, et comment vous pouvez utiliser le meme modele aujourd'hui via des heritiers open source comme OpenFGA, sans faire tourner l'infrastructure de Google.
La question qui parait simple et ne l'est pas
Commencons par la version que tout le monde ecrit en premier. Les permissions vivent a cote de la ressource, et on les verifie en ligne.
func canView(user User, doc Document) bool {
if doc.OwnerID == user.ID {
return true
}
for _, id := range doc.SharedWith {
if id == user.ID {
return true
}
}
return false
}
Le premier jour, ca va. Le probleme n'est pas que c'est faux, c'est que ca ne se compose pas, et l'autorisation est entierement une affaire de composition :
- Heritage. Un document dans un dossier devrait heriter des lecteurs du dossier. Voila
canViewqui doit connaitre les dossiers. Les dossiers s'imbriquent, donc il lui faut de la recursion. - Groupes. On partage avec une equipe, pas avec une liste d'ID utilisateurs. Les equipes contiennent des equipes. Voila
canViewqui parcourt un graphe de groupes. - Duplication. Docs, Drive et Photos font chacun grandir leur propre copie de cette logique, et elles divergent. Un bug de permission dans l'un n'est pas un bug dans les autres, ce qui est pire, car on ne peut plus raisonner sur le systeme dans son ensemble.
- Audit. "Pourquoi Bob a-t-il l'acces ?" n'a pas de reponse, car l'acces est un calcul disperse entre services et lignes de base, pas un fait qu'on peut consulter.
- Coherence. Vous revoquez Bob et changez aussitot ce qu'il regardait. Si un cache sert l'ancienne reponse ne serait-ce qu'un instant, Bob voit du contenu qu'il ne devrait pas. L'article appelle cela le probleme du "nouvel ennemi" (new enemy), et c'est toute la raison pour laquelle Zanzibar est complique.
RBAC (roles) et ABAC (attributs) resolvent chacun une partie de cela et aucun ne resout la forme du probleme, car la vraie forme est un graphe : les utilisateurs sont lies a des groupes, les groupes a des dossiers, les dossiers a des documents, et un controle de permission est une question d'accessibilite dans ce graphe. Ce recadrage est l'idee entiere derriere Zanzibar.
L'idee centrale : les permissions sont des relations
Zanzibar stocke l'autorisation sous une seule sorte de fait uniforme appelee tuple de relation. Lisez-en un a voix haute et c'est une phrase :
document:readme#viewer@user:alice
"Sur l'objet document:readme, la relation viewer inclut user:alice." Alice peut voir le readme. Chaque permission de toute l'entreprise se reduit a un tas de ces tuples de la forme object#relation@subject. Il n'y a pas de schema propre a chaque produit avec ses colonnes et ses tables de jointure ; il y a une seule table de relations, et un controle de permission est une requete de graphe dessus.
C'est le ReBAC, controle d'acces base sur les relations (relationship-based access control). L'intuition est que "role" et "attribut" ne sont que des cas particuliers de "relation". Proprietaire est une relation. Appartenance est une relation. Dossier parent est une relation. Une fois que tout est le meme genre d'arete, un seul moteur peut repondre a chaque question, et l'audit vient gratuitement : "pourquoi Bob a-t-il l'acces ?" devient "montre-moi le chemin de tuples de Bob vers ce document", ce qui est une requete, pas une fouille archeologique.
Le sujet a droite du @ peut lui-meme etre un ensemble d'utilisateurs, pas juste un seul, et c'est la que le modele tire sa puissance. Au lieu de nommer Alice, on pointe vers tous ceux qui sont membres d'un groupe :
document:readme#viewer@group:engineering#member
"Tous ceux de la relation member de group:engineering sont lecteurs du readme." C'est un userset : une reference vers "les membres de cette relation sur cet objet". Les usersets sont la maniere dont les groupes, l'imbrication et l'heritage s'effondrent tous dans le meme format de tuple. Un groupe avec des sous-groupes est juste un tuple dont le sujet est le userset member d'un autre groupe. Le graphe se construit tout seul.
Le modeliser avec OpenFGA
Zanzibar lui-meme est interne a Google, mais son modele est desormais un standard ouvert que vous pouvez faire tourner. OpenFGA (un projet CNCF, ne chez Auth0/Okta) est l'implementation la plus utilisee, et son DSL rend le modele concret. Un modele a des types (les sortes d'objets), et chaque type declare des relations.
Commencons par le modele utile le plus simple : des documents avec trois roles, ou chaque role implique le plus faible.
model
schema 1.1
type user
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user] or editor
Lisez les regles de reecriture a droite. editor est "quiconque assigne directement editor, ou quiconque est owner". viewer est "quiconque assigne directement viewer, ou quiconque est editor". Donc les owners sont editors sont viewers, gratuitement, sans tuple duplique. Ce or est une union d'ensembles, la meme union que la config de Zanzibar appelle userset rewrite. Le moteur la calcule au moment du controle ; vous ne stockez jamais les tuples derives.
Maintenant les faits de permission reels, les tuples :
[
{ "user": "user:alice", "relation": "owner", "object": "document:readme" },
{ "user": "user:bob", "relation": "viewer", "object": "document:readme" }
]
Alice en est proprietaire, Bob peut le voir. Un controle donne :
check(user:alice, viewer, document:readme) → allowed: true // owner ⇒ editor ⇒ viewer
check(user:bob, editor, document:readme) → allowed: false // seulement viewer direct
Alice est lectrice bien qu'aucun tuple ne le dise, car le modele le derive. C'est toute l'astuce, en petit.
Les groupes, sans jamais retoucher le document
Ajoutons un type groupe et laissons un document etre partage avec les membres d'un groupe. C'est le userset de tout a l'heure, en syntaxe OpenFGA.
type group
relations
define member: [user]
type document
relations
define owner: [user]
define editor: [user] or owner
define viewer: [user, group#member] or editor
Le [user, group#member] dit que la relation viewer accepte soit un utilisateur direct, soit le userset member d'un groupe. Partageons maintenant avec toute l'equipe engineering en un tuple :
[
{ "user": "group:engineering#member", "relation": "viewer", "object": "document:readme" },
{ "user": "user:carol", "relation": "member", "object": "group:engineering" }
]
Carol peut maintenant voir le readme, et vous n'avez jamais touche au document pour l'accorder. Ajoutez quelqu'un a group:engineering#member et il gagne l'acces a chaque document que ce groupe peut voir, instantanement et en un seul endroit. Les groupes peuvent contenir des groupes de la meme facon, en faisant accepter group#member au member d'un groupe, et le controle parcourt simplement la chaine.
L'heritage : le dossier qui partage son acces vers le bas
Le dernier mouvement central est le tuple-to-userset, le nom que Zanzibar donne a "suivre une relation vers un autre objet et heriter de sa relation". C'est ainsi que les lecteurs d'un dossier deviennent les lecteurs de chaque document a l'interieur. OpenFGA l'ecrit X from Y.
type folder
relations
define viewer: [user, group#member]
type document
relations
define parent: [folder]
define owner: [user]
define editor: [user] or owner
define viewer: [user, group#member] or editor or viewer from parent
La nouvelle clause viewer from parent signifie "aussi quiconque est viewer de l'objet dans ma relation parent". Reliez un document a un dossier avec un tuple et tout l'ensemble des lecteurs du dossier coule vers le bas :
[
{ "user": "folder:handbook", "relation": "parent", "object": "document:readme" },
{ "user": "user:dave", "relation": "viewer", "object": "folder:handbook" }
]
Dave peut voir le readme parce qu'il peut voir son dossier parent. Imbriquez des dossiers dans des dossiers et l'acces se propage jusqu'en bas, evalue a la demande. Trois primitives, l'union (or), l'intersection (and, pour "doit etre les deux"), et le tuple-to-userset (from), suffisent a exprimer quasiment tout schema de permission du monde reel. Cette economie est la raison pour laquelle le modele a tenu.
La vraie partie difficile : la coherence a l'echelle globale
Tout ce qui precede est un modele de donnees propre. Il serait aussi inutile a l'echelle de Google sans resoudre le probleme que le modele a discretement cree : ce graphe est enorme, shardе a travers la planete, mis en cache partout, et lu des milliards de fois par jour. On ne peut pas re-parcourir le graphe complet a chaque controle. Donc on met en cache. Et des l'instant ou on met l'autorisation en cache, on herite du probleme du nouvel ennemi.
Imaginez. Alice retire Bob d'un document, puis le modifie pour ajouter un secret. Deux ecritures, dans cet ordre, avec un lien causal : elle a revoque *parce qu'*elle allait ajouter le secret. Si une replique quelque part a quelques secondes de retard et pense encore que Bob est lecteur, Bob lit le secret. Chaque reponse individuelle etait "correcte" pour un instant passe, mais les servir dans le desordre viole l'ordre causal sur lequel l'utilisateur comptait. Les lectures perimees sont d'habitude une note de bas de page de performance. En autorisation, c'est une faille de securite.
Le remede de Zanzibar est un jeton d'instantane qu'il appelle zookie. Quand l'ACL change, l'appelant recoit un zookie, un jeton opaque encodant un horodatage. Quand l'application demande plus tard "Bob peut-il voir ceci ?", elle passe le zookie correspondant au contenu qu'elle s'apprete a montrer. Zanzibar garantit alors que le controle est evalue sur un instantane au moins aussi frais que ce jeton. Il a le droit de lire depuis une replique bon marche et un peu perimee, mais jamais plus vieille que le contenu protege. La regle en une ligne : le controle de permission ne doit jamais etre plus perime que la donnee qu'il protege. Les zookies rendent "assez frais" precis au lieu d'esperer que chaque replique soit a jour.
En dessous, cela s'appuie sur Spanner et son horloge TrueTime, qui donne a Zanzibar des horodatages globalement ordonnes et une coherence externe : une ecriture terminee avant une autre obtient vraiment un horodatage plus ancien, partout, sans verrou global sur le chemin de lecture. Les lectures d'instantane a un horodatage de peremption bornee sont ce qui permet a Zanzibar de servir la plupart des controles depuis des repliques locales en quelques millisecondes tout en refusant de servir une reponse perimee dangereuse. La coherence n'est pas gratuite, elle est achetee avec TrueTime, et cette dependance explique en grande partie pourquoi l'article se lit comme de la recherche en systemes distribues et non un tutoriel CRUD.
L'autre astuce d'echelle a connaitre par son nom est Leopard, un index qui pre-aplatit l'appartenance a des groupes profondement imbriques. Certains groupes s'imbriquent sur des dizaines de niveaux avec des millions de membres, et parcourir cela en direct a chaque controle exploserait le budget de latence. Leopard maintient une representation d'ensemble denormalisee et mise a jour de facon incrementale, de sorte qu'une question "X est-il membre de ce geant groupe imbrique ?" devienne une recherche d'ensemble rapide plutot qu'un parcours de graphe profond. C'est le cache specialise qui garde le modele general rapide sur ses entrees les plus difficiles.
Les chiffres, pour que la complexite ait du sens
Il est facile de lire tout cela et de penser que c'est sur-ingenie. L'echelle est la raison pour laquelle ca ne l'est pas. D'apres l'article : Zanzibar sert plus de 10 millions de controles d'autorisation par seconde, en garde plus de 95 % sous 10 millisecondes (et 99 % sous une borne de quelques dizaines de ms), detient des milliers de milliards de tuples de relation sur plus de 1 500 serveurs dans des dizaines de clusters dans le monde, et tourne a plus de 99,999 % de disponibilite depuis des annees. Chacune des parties difficiles, zookies, Spanner, Leopard, existe pour tenir l'un de ces chiffres sans en casser un autre. La coherence sans la latence. La latence sans la securite perimee. L'echelle sans ni l'un ni l'autre.
Vous n'avez pas besoin de Google pour utiliser ceci
Vous ne ferez presque certainement jamais tourner Zanzibar. Vous n'en avez pas besoin. Le modele a survecu a l'article sous la forme d'une famille de moteurs open source que vous pouvez deployer cet apres-midi :
- OpenFGA (CNCF, issu d'Auth0/Okta), utilise dans tout cet article. DSL accessible, API HTTP et gRPC, SDK dans la plupart des langages.
- SpiceDB (AuthZed). L'implementation la plus fidele a l'article, avec un vrai equivalent du zookie qu'elle appelle ZedToken et des modes de coherence reglables.
- Ory Keto, une implementation Go plus ancienne du modele de tuples.
Le bouton de coherence apparait directement dans ces outils. OpenFGA laisse une requete Check passer une preference consistency : MINIMIZE_LATENCY lit depuis le cache et est le defaut, tandis que HIGHER_CONSISTENCY force une evaluation fraiche, le meme compromis que les zookies encodent, expose comme un choix par appel. Le ZedToken de SpiceDB en est l'analogue le plus proche : vous stockez le jeton recu d'une ecriture et le passez aux controles ulterieurs pour exiger "au moins aussi frais que ceci".
Une facon pragmatique de l'adopter : gardez les donnees de votre app la ou elles sont, et deplacez seulement la decision d'autorisation dans le moteur ReBAC. A chaque ecriture qui change l'acces, poussez un tuple. A chaque controle, demandez au moteur. Vous obtenez la source unique de verite, le chemin d'audit et le modele d'heritage sans reecrire votre base de donnees.
RBAC, ABAC, ReBAC : quand choisir lequel
Le ReBAC n'est pas automatiquement la reponse. Adaptez le modele a la forme de vos permissions.
| Modele | Repond via | Brille quand | Peine quand |
|---|---|---|---|
| RBAC (roles) | "Quel role a l'utilisateur ?" | Petit jeu fixe de roles, ressources plates | Partage par objet, heritage, "qui peut voir X ?" |
| ABAC (attributs) | "Les attributs utilisateur/ressource satisfont-ils une politique ?" | Regles sur le contexte (heure, region, habilitation) | Relations et imbrication ; audit du pourquoi |
| ReBAC (relations) | "Y a-t-il un chemin de relations de l'utilisateur a l'objet ?" | Partage, groupes, dossiers, hierarchies, droits par objet | Regles purement d'attributs sans relations |
La plupart des vrais systemes sont un melange, et la bonne nouvelle est que les moteurs ReBAC laissent de plus en plus attacher des conditions (OpenFGA a des tuples conditionnels, des controles contextuels facon and) pour replier un peu d'ABAC dans le graphe quand vous voulez "editor et seulement pendant les heures ouvrees".
A retenir
- L'autorisation est un probleme de graphe deguise. Des que vous avez du partage, des groupes ou de l'heritage, "cet utilisateur peut-il faire ceci ?" est une question d'accessibilite, et la modeliser en
ifdisperses est la raison pour laquelle elle pourrit. - Un seul format de tuple exprime tout.
object#relation@subject, avec les usersets et les regles de reecriture, effondre roles, groupes, imbrication et heritage dans un seul modele uniforme qu'un moteur peut raisonner et auditer. - Union, intersection et tuple-to-userset sont tout le langage.
or,andetfromdans OpenFGA couvrent quasiment tout schema de permission que vous rencontrerez. - Mettre l'autorisation en cache cree le probleme du nouvel ennemi, et les zookies le resolvent. Le controle ne doit jamais etre plus perime que la donnee qu'il protege. Zanzibar achete cette garantie avec le TrueTime de Spanner ; le ZedToken de SpiceDB et les modes de coherence d'OpenFGA vous exposent le meme compromis.
- Vous pouvez avoir le modele sans l'infrastructure. OpenFGA, SpiceDB et Ory Keto mettent le design de Zanzibar a portee aujourd'hui. Deplacez la decision, pas vos donnees, et vous obtenez une source unique de verite et une vraie reponse a "pourquoi Bob a-t-il l'acces ?".
La lecon durable de Zanzibar n'est pas les dix millions de controles par seconde. C'est le recadrage : cessez de traiter la permission comme un booleen calcule a la hate, et commencez a la traiter comme une relation que vous stockez, pour que la reponse a chaque question d'acces soit un fait que vous pouvez consulter, expliquer et croire.
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
De RBAC a ReBAC : migrer un systeme de roles vers OpenFGA sans coupure
Les roles marchent, jusqu'au jour ou quelqu'un dit 'partage juste ce document avec juste cette personne'. Ce jour-la, RBAC atteint ses limites. Voici la migration pratique : mapper les tables de roles vers des tuples de relation, faire tourner OpenFGA en shadow a cote de vos controles SQL, backfiller sans risque, puis seulement debloquer le partage par objet et la hierarchie que les roles n'ont jamais pu offrir. Avec les pieges dont personne ne parle.
July 19, 2026
Algorithmes de load balancing : chacun repare le defaut du precedent
Repartir des requetes sur N serveurs sonne comme un one-liner : choisir un serveur, envoyer la requete. Puis un backend est plus lent, ou plus gros, ou tient une session, et le choix naif s'effondre. On parcourt les algorithmes classiques comme une chaine ou chacun existe pour reparer l'angle mort du precedent : round robin, pondere, least connections, power of two choices, et consistent hashing, en Go, jusqu'a la panne que tous les tutos oublient.
July 20, 2026
Retry Storms : comment de bons clients font tomber des serveurs en bonne santé
Un retry semble inoffensif : la requête a échoué, on réessaie. Multipliez ça par chaque client, ajoutez une dépendance lente, et les retries deviennent un DDoS auto-infligé. On part de la boucle de retry naïve vers le backoff exponentiel, le jitter, les retry budgets et les circuit breakers, la moitié côté client de la résilience, qui complète le rate limiting côté serveur.
July 11, 2026