Aller au contenu

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 RBACVit commeEquivalent ReBAC
user_roles(bob, admin)une lignetuple organization:acme#admin@user:bob
le role adminune tableune relation sur un type
role_permissions(admin, doc.edit)une ligneune regle de reecriture dans le modele, pas un tuple
le controle de permission (jointure SQL)une requeteun 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.

  1. Backfill. Script unique : lire chaque ligne user_roles et ecrire le tuple equivalent. Idempotent, car ecrire deux fois le meme tuple est un no-op. Lancez-le, puis reconciliez les comptes.
  2. 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.
  3. 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.
  4. 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.

Propulsé par Buttondown.

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.

#Authorization #ReBAC #Security #Distributed-Systems #OpenFGA

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.

#Load-Balancing #Go #Distributed-Systems #Resilience

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.

#Resilience #Distributed-Systems #Go #Retries

July 11, 2026

© 2026 < Denis AKPAGNONITE /> | N1BBzerLZXT