De RBAC a ReBAC : migrer un systeme de roles vers OpenFGA sans coupure
19 juillet 2026 · 10 min read · Read in English
Sommaire
Le controle d'acces base sur les roles (RBAC) est le modele d'autorisation vers lequel tout le monde se tourne en premier, et pour de bonnes raisons : il est simple, il tient dans quelques tables, et pendant longtemps il suffit exactement. Vous avez des roles, vous les assignez aux utilisateurs, les roles portent des permissions, et un controle est une jointure. Puis un jour, quelqu'un au produit prononce la phrase qui met fin a l'utilite de RBAC : "laisse le proprietaire partager ce document precis avec cette personne precise." Et votre modele de roles bien propre n'a nulle part ou ranger ce fait.
Ceci est le compagnon pratique de la presentation de Zanzibar. Celle-la explique pourquoi le controle d'acces base sur les relations fonctionne. Celle-ci suppose que le modele vous a convaincu et que vous avez maintenant un vrai systeme RBAC en production a deplacer vers OpenFGA sans coupure et sans bascule big-bang effrayante. On va mapper les tables, faire tourner les deux systemes cote a cote, backfiller, basculer le chemin de lecture, et seulement ensuite ajouter les fonctionnalites que les roles n'ont jamais pu offrir. En chemin, les pieges qui rendent cela plus dur que les articles ne l'avouent.
La ou RBAC atteint ses limites
Voici le RBAC que la plupart des equipes font vraiment tourner. Trois tables et une jointure.
CREATE TABLE roles (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE -- 'admin', 'editor', 'viewer'
);
CREATE TABLE user_roles (
user_id INT,
role_id INT
);
CREATE TABLE role_permissions (
role_id INT,
permission TEXT -- 'document.edit', 'document.view'
);
Un controle de permission est une seule requete : cet utilisateur detient-il un role qui accorde cette permission ?
SELECT EXISTS (
SELECT 1 FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
WHERE ur.user_id = $1 AND rp.permission = $2
);
C'est propre et rapide, et cela repond a exactement une forme de question : que peut faire cet utilisateur, globalement, en vertu de son role ? Le mur que vous heurtez, c'est que les vrais produits arretent de poser cette question. Ils commencent a demander :
- "Bob peut-il editer le document 42 ?" Pas les documents en general. Celui-la. RBAC n'a pas de colonne pour l'objet.
- "Partage ce dossier avec l'equipe design, et que ca cascade vers chaque fichier a l'interieur." RBAC n'a pas de hierarchie.
- "Pourquoi Bob voit-il ce fichier ?" Avec les roles, la reponse est "il est editor", ce qui n'explique pas quel droit, car les roles sont globaux et les objets ne le sont pas.
Vous pouvez greffer des ID d'objet sur les chaines de permission (document:42:edit) et regarder les tables exploser, ou accepter que le modele a la mauvaise forme pour l'acces par objet. RBAC repond a quel role ; le produit demande desormais quel objet, et cet ecart est toute la raison de migrer.
Le declic mental : un role n'est qu'une relation
L'astuce pour une migration sereine est d'arreter de voir ReBAC comme un autre univers. Ce sont les memes faits, deplaces. En RBAC, "Bob est admin" vit comme une ligne dans user_roles. En ReBAC, cela vit comme un tuple : une relation entre Bob et un objet.
| Concept RBAC | Vit comme | Equivalent ReBAC |
|---|---|---|
user_roles(bob, admin) | une ligne | tuple organization:acme#admin@user:bob |
le role admin | une table | une relation sur un type |
role_permissions(admin, doc.edit) | une ligne | une regle de reecriture dans le modele, pas un tuple |
| le controle de permission (jointure SQL) | une requete | un appel Check d'OpenFGA |
Deux choses se deplacent differemment, et c'est le noeud. Les assignations de role deviennent des tuples (des donnees que vous ecrivez). Les permissions deviennent des regles du modele (un schema defini une fois). En RBAC, les deux vivent dans des tables ; en ReBAC, le "qui a quel role" est de la donnee et le "quel role accorde quoi" est le modele. Mettez ce partage au clair et le reste est mecanique.
Etape 1 : reproduire votre RBAC actuel, a l'identique
N'ajoutez pas encore une seule fonctionnalite. Le premier geste est de modeliser vos roles existants dans OpenFGA pour que le comportement soit identique. Les roles globaux se mappent vers des relations sur un seul objet organization auquel tout appartient.
model
schema 1.1
type user
type organization
relations
define admin: [user]
define editor: [user] or admin
define viewer: [user] or editor
Notez que les reecritures gagnent deja leur place : admin implique editor implique viewer, ce qu'en RBAC vous deviez encoder a la main dans role_permissions. Vos assignations de role globales deviennent des tuples contre l'org :
[
{ "user": "user:bob", "relation": "admin", "object": "organization:acme" },
{ "user": "user:carol", "relation": "viewer", "object": "organization:acme" }
]
Et l'ancien controle de permission devient un Check :
check(user:bob, editor, organization:acme) → allowed: true // admin ⇒ editor
Le but de cette etape est la parite, pas le progres. Toute question que votre SQL savait repondre, OpenFGA la repond desormais de la meme facon. Rien ne change dans le produit. Vous avez simplement deplace la verite, ce qui est exactement ce qui rend l'etape suivante sure.
Etape 2 : faire tourner OpenFGA en shadow
Ne basculez jamais l'autorisation en un seul commit. Faites tourner les deux systemes en parallele et laissez-les se contredire bruyamment avant de faire confiance au nouveau. Gardez le controle SQL comme autorite, et a chaque controle interrogez aussi OpenFGA pour comparer.
func canEdit(ctx context.Context, userID string, docID int) bool {
sqlAllowed := sqlCheck(ctx, userID, "document.edit") // toujours la source de verite
// shadow : interroger OpenFGA, comparer, mais ne pas encore agir dessus
fgaAllowed, err := fga.Check(ctx, fgaReq(userID, "editor", "organization:acme"))
if err != nil {
log.Warn("fga shadow check failed", "err", err)
} else if fgaAllowed != sqlAllowed {
log.Warn("authz mismatch",
"user", userID, "doc", docID,
"sql", sqlAllowed, "fga", fgaAllowed) // enqueter sur chacun
}
return sqlAllowed
}
Laissez cela tourner en production une semaine ou deux. Chaque desaccord journalise est un bug dans votre modele ou dans votre ecriture de tuples, trouve avant de pouvoir refuser un utilisateur legitime ou fuiter un document. Quand le journal des desaccords devient silencieux, vous avez gagne le droit de faire confiance a OpenFGA. Cette phase shadow est la partie la plus importante de la migration et celle que la plupart des guides sautent.
Pendant le shadow, cablez les doubles ecritures : partout ou vous inserez aujourd'hui dans user_roles, ecrivez aussi le tuple correspondant. Les nouvelles assignations atterrissent desormais dans les deux systemes, donc les deux ne divergent pas pendant que vous preparez la bascule.
Etape 3 : ajouter ce que RBAC n'a jamais pu
Une fois le shadow propre, la recompense. Les fonctionnalites qui ont force cette migration sont maintenant de petits ajouts au modele, sans migration de schema ni explosion de tables. Introduisez un type document avec des droits par objet et l'heritage de dossier.
type group
relations
define member: [user]
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
Maintenant, la phrase qui cassait RBAC tient en un tuple :
[
{ "user": "user:bob", "relation": "viewer", "object": "document:42" }
]
Bob peut voir le document 42 et rien d'autre. Partagez un dossier avec une equipe et ca cascade vers chaque document a l'interieur via viewer from parent. Ajoutez quelqu'un a un groupe et il herite de chaque droit que le groupe detient. Rien de tout cela n'a demande une table, une migration ou de toucher aux documents. Le meme moteur qui a reproduit vos anciens roles exprime maintenant ce que les roles n'ont jamais pu, car les relations se composent et les lignes de role non.
Votre controle pour un document precis cible desormais cet objet plutot que l'org :
check(user:bob, viewer, document:42) → allowed: true
Etape 4 : backfill, verifier, basculer
La bascule a un ordre strict. Faites-la dans le desordre et vous refusez de vrais utilisateurs ou servez des autorisations perimees.
- Backfill. Script unique : lire chaque ligne
user_roleset ecrire le tuple equivalent. Idempotent, car ecrire deux fois le meme tuple est un no-op. Lancez-le, puis reconciliez les comptes. - Verifier. Laissez la comparaison shadow de l'etape 2 continuer contre les donnees backfillees. Zero desaccord sur une fenetre significative est votre feu vert. Ne precipitez pas.
- Basculer le chemin de lecture. Changez le controle autoritaire de SQL vers OpenFGA. Le code shadow s'inverse : desormais OpenFGA decide et (brievement) SQL est celui contre lequel vous journalisez, au cas ou.
- Retirer. Apres une periode stable, supprimez le controle SQL et, a terme, les tables de roles. Gardez la double ecriture jusqu'a etre certain que rien d'autre ne les lit.
La raison pour laquelle cet ordre importe est la meme preoccupation de coherence que l'article Zanzibar : un controle ne doit jamais etre plus perime que la donnee qu'il protege. Pendant le backfill, des tuples sont encore ecrits, donc gardez SQL autoritaire jusqu'a ce que l'ensemble de tuples soit prouve complet. Basculez seulement quand c'est frais.
Les pieges dont personne ne parle
Ne portez pas votre proliferation de roles. Si RBAC a grossi jusqu'a quarante roles parce que les roles etaient votre seul outil de cadrage, resistez a recreer quarante relations. Une grande part de cette proliferation venait de gens encodant l'objet et le contexte dans les noms de roles (editor_marketing_eu). En ReBAC, le cadrage par objet est un tuple et le contexte est une condition. Migrez l'intention, pas les quarante roles.
Verifier un objet est bon marche ; lister ne l'est pas. Check(user, viewer, document:42) est rapide. Mais "montre-moi chaque document que Bob peut voir" est ListObjects, qui parcourt le graphe a l'envers et est une operation fondamentalement plus lourde. Si votre UI liste les ressources par utilisateur, concevez pour cela tot : OpenFGA a ListObjects, mais comprenez son cout et mettez en cache en consequence. Le WHERE role IN (...) de RBAC etait bon marche ici ; ne supposez pas la parite.
Gardez la base de donnees et les tuples synchronises, de facon transactionnelle. Le mode d'echec des systemes doubles est la derive : vous committez une ligne et l'ecriture du tuple echoue, donc les deux se contredisent. Ecrivez le tuple dans la meme transaction que le changement metier quand vous le pouvez, ou utilisez un outbox (enregistrez l'intention dans votre base, un worker la synchronise vers OpenFGA avec des retries). Une ecriture de tuple en fire-and-forget finira par se desynchroniser et produira exactement les tickets "pourquoi Bob ne voit-il pas ceci ?" que vous avez migre pour eviter.
Modeliser, puis migrer. Les plus gros gains et les plus grosses erreurs se produisent tous deux dans le fichier de modele, avant qu'un seul tuple ne bouge. Prenez le temps de bien poser les types et les reecritures ; un mauvais modele backfille dans des millions de tuples coute cher a defaire.
A retenir
- RBAC atteint ses limites a l'acces par objet. Le jour ou vous devez partager une ressource avec une personne, les roles n'ont nulle part ou le stocker. Ce besoin est le declencheur de la migration, pas un confort.
- Un role n'est qu'une relation, et une permission n'est qu'une regle du modele. Les assignations deviennent des tuples (donnees) ; le "quel role accorde quoi" devient des reecritures (schema). Garder ce partage clair rend le mapping mecanique.
- Reproduire avant d'ameliorer. Modelisez d'abord vos roles existants et prouvez la parite, pour que la migration soit un deplacement, pas une reecriture. Seulement ensuite ajoutez hierarchie et partage par objet.
- Shadow, puis basculer dans l'ordre. Faites tourner OpenFGA a cote de SQL jusqu'a ce que les desaccords se taisent, backfillez, verifiez, basculez, retirez. Jamais en un seul commit.
- Surveillez les trois pieges : ne portez pas la proliferation de roles, budgetez pour
ListObjects, et gardez tuples et lignes synchronises avec une transaction ou un outbox.
RBAC n'est pas faux ; c'est un modele qui repond bien a une question et s'epuise quand les questions changent de forme. ReBAC ne remplace pas l'idee de role, il la generalise : un role devient une sorte de relation parmi d'autres, et la migration est surtout le travail d'admettre que vos permissions ont toujours ete un graphe, et de finir par les stocker comme tel.
S'abonner aux prochains articles
Recevez les nouveaux articles par e-mail. Pas de spam, désinscription à tout moment.
Related posts
Zanzibar demystifie : comment Google repond a 'cet utilisateur a-t-il le droit ?'
L'autorisation ressemble a un if d'une ligne, jusqu'a ce qu'on l'execute dix millions de fois par seconde sur tous les produits de Google. Zanzibar est le systeme qui a rendu cette question rapide, coherente et globale. On part du controle de permission naif pour arriver au controle d'acces base sur les relations, au modele de tuples, a la coherence avec les zookies, et aux heritiers open source comme OpenFGA que vous pouvez utiliser des aujourd'hui.
July 18, 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