Authentification et sécurité web - 06 - RBAC : rôles, permissions et middleware

Implementer un système de rôles et permissions en TypeScript. RBAC, middleware et les erreurs classiques.

06 - RBAC : rôles, permissions et middleware

Ce que tu vas apprendre

  • Ce qu'est le RBAC et comment le modéliser
  • Comment implementer un middleware de permissions en TypeScript
  • Les erreurs classiques a éviter

Prerequisites

Avoir lu l'article sur l'authentification vs autorisation. Connaitre les bases d'un framework backend (Elysia, Express, Fastify).


RBAC en deux phrases

RBAC, c'est Rôle-Based Access Control. Les utilisateurs ont des rôles, les rôles ont des permissions, et ton code vérifié les permissions. Pas les rôles.

Cette dernière phrase est la plus importante de l'article. Je la répété : ton code vérifié les permissions, pas les rôles. On va voir pourquoi.

Le modèle

typescript// Definition des roles et de leurs permissions
const ROLES = {
  admin: ["articles:read", "articles:write", "articles:delete", "users:manage"],
  editor: ["articles:read", "articles:write"],
  viewer: ["articles:read"],
} as const;

type Role = keyof typeof ROLES;
type Permission = (typeof ROLES)[Role][number];

function hasPermission(userRole: Role, permission: Permission): boolean {
  return (ROLES[userRole] as readonly string[]).includes(permission);
}

J'utilise un format resource:action pour les permissions. C'est lisible, ca se grep facilement, et ca évité les ambiguites. "write" tout court ne veut rien dire. "articles:write" est clair.

Le middleware

Sur paltemps.fr, l'autorisation est binaire : tu es admin ou tu ne l'es pas (IP whitelist + mot de passe). Mais sur un projet plus complexe avec des vrais utilisateurs, voila comment je structure le middleware avec Elysia :

typescript// Middleware generique pour verifier une permission
function requirePermission(permission: Permission) {
  return (ctx: { user?: { role: Role } }) => {
    if (!ctx.user) {
      return new Response(JSON.stringify({ error: "Unauthenticated" }), {
        status: 401,
      });
    }

    if (!hasPermission(ctx.user.role, permission)) {
      return new Response(JSON.stringify({ error: "Forbidden" }), {
        status: 403,
      });
    }
  };
}

// Utilisation sur les routes
app.delete("/api/articles/:id",
  { beforeHandle: requirePermission("articles:delete") },
  async ({ params }) => {
    await db.query("DELETE FROM articles WHERE id = $1", [params.id]);
    return { ok: true };
  }
);

app.get("/api/articles",
  { beforeHandle: requirePermission("articles:read") },
  async () => {
    return await db.query("SELECT * FROM articles");
  }
);

Remarque les codes HTTP : 401 si l'utilisateur n'est pas authentifié, 403 s'il est authentifié mais n'a pas la permission. Comme on l'a vu dans l'introduction, ce sont deux erreurs différentes.

Pourquoi vérifier les permissions et pas les rôles

Je vois ca tout le temps en code review :

typescript// BAD : on verifie le role
if (user.role === "admin") {
  // autoriser la suppression
}

Ca marche aujourd'hui. Mais demain, tu ajoutes un rôle "moderator" qui peut aussi supprimer des articles. Tu dois chercher tous les === "admin" dans ton code et ajouter || user.role === "moderator". Tu en oublies un. Tu as une faille.

Avec les permissions :

typescript// GOOD : on verifie la permission
if (hasPermission(user.role, "articles:delete")) {
  // autoriser la suppression
}

Ajouter un rôle "moderator" qui peut supprimer ? Tu modifies un seul endroit :

typescriptconst ROLES = {
  admin: ["articles:read", "articles:write", "articles:delete", "users:manage"],
  moderator: ["articles:read", "articles:write", "articles:delete"],
  editor: ["articles:read", "articles:write"],
  viewer: ["articles:read"],
} as const;

Le reste du code ne change pas. Un seul point de modification, c'est ca la force du RBAC bien implemente.

RBAC vs ABAC

ABAC, c'est Attribute-Based Access Control. Au lieu de rôles fixes, les décisions d'autorisation se basent sur des attributs : l'utilisateur, la ressource, le contexte.

Exemple : "un utilisateur peut modifier un article s'il en est l'auteur ET si l'article n'est pas publie". Ca ne rentre pas dans du RBAC pur parce que la permission depend de la ressource spécifique.

typescript// ABAC : la permission depend du contexte
function canEditArticle(user: User, article: Article): boolean {
  if (hasPermission(user.role, "articles:write")) {
    // Les admins et editeurs peuvent tout modifier
    if (user.role === "admin") return true;
    // Les editeurs ne peuvent modifier que leurs propres articles non publies
    return article.authorId === user.id && article.status !== "published";
  }
  return false;
}

En pratique, la plupart des applications commencent avec du RBAC et ajoutent des regles ABAC ponctuelles quand c'est nécessaire. Ne pars pas directement sur un système ABAC complet si tu n'en as pas besoin.

Les erreurs classiques

Le rôle "god admin" sans limites. Un admin qui peut tout faire, y compris supprimer la base de donnees ou modifier les autres admins. Le principe du moindre privilege s'applique aussi aux admins. Sur les systèmes critiques, meme les admins ont des contraintes (audit log, double validation).

Verifier l'autorisation cote frontend uniquement. Cacher un bouton "Supprimer" dans le HTML ne protégé rien. Un curl ou un F12 dans le navigateur contourne ca en 5 secondes. L'autorisation se vérifié toujours cote serveur. Le frontend peut masquer les éléments pour l'ergonomie, mais le backend est le seul garant.

Pas de vérification sur les endpoints API. J'ai vu des APIs ou /api/users/42 retourne les donnees de n'importe quel utilisateur si tu es connecte, sans vérifier que tu es bien l'utilisateur 42 ou un admin. C'est un problème d'autorisation classique (IDOR, Insecure Direct Object Référence).

Oublier de recharger les permissions. Si tu caches les permissions au login et que le rôle change ensuite (un admin revoque les droits), l'utilisateur garde ses anciennes permissions jusqu'a sa prochaine connexion. Avec des sessions, c'est facile de recharger. Avec des JWT, le rôle est dans le token et ne change qu'a l'expiration.


Navigation : Precedent : 05 - Hashing | Suivant : 07 - XSS, CSRF, injection


Sources

Retrouve d'autres articles techniques sur paltemps.fr.

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.