Les menus sensibles aux permissions améliorent la clarté, mais la sécurité doit rester côté backend. Découvrez des schémas simples pour rôles, politiques et masquage sûr de l'interface.

Quand les gens disent « masquer le bouton », ils veulent généralement dire l'une des deux choses : réduire l'encombrement pour les utilisateurs qui ne peuvent pas utiliser une fonctionnalité, ou empêcher les usages abusifs. Seul le premier objectif est réaliste côté frontend.
Les menus sensibles aux permissions sont principalement un outil UX. Ils aident quelqu'un à ouvrir l'application et à voir immédiatement ce qu'il peut faire, sans tomber sur des écrans « Accès refusé » à chaque autre clic. Ils réduisent aussi la charge du support en évitant des confusions du type « Où approuve-t-on les factures ? » ou « Pourquoi cette page plante ? »
Masquer l'interface n'est pas de la sécurité. C'est de la clarté.
Même un collègue curieux peut toujours :
Donc le vrai problème que résolvent les menus sensibles aux permissions est une guidance honnête. Ils gardent l'interface alignée avec le travail, le rôle et le contexte de l'utilisateur, tout en rendant évident ce qui n'est pas disponible.
Un bon état final ressemble à ceci :
Exemple : dans un petit CRM, un Commercial doit voir Leads et Tâches, mais pas la Gestion des utilisateurs. S'il colle quand même l'URL de gestion des utilisateurs, la page doit échouer en mode fermé, et le serveur doit toujours bloquer toute tentative de lister les utilisateurs ou de changer des rôles.
La visibilité est ce que l'interface choisit d'afficher. L'autorisation est ce que le système permettra réellement lorsqu'une requête atteint le serveur.
Les menus sensibles aux permissions réduisent la confusion. Si quelqu'un ne sera jamais autorisé à voir Facturation ou Admin, masquer ces éléments garde l'app propre et diminue les tickets de support. Mais masquer un bouton n'est pas un cadenas. Les gens peuvent toujours essayer l'endpoint sous-jacent avec les outils dev, un ancien favori, ou une requête copiée.
Règle pratique : décidez de l'expérience que vous voulez, puis faites appliquer la règle côté backend quoi qu'il arrive.
Quand vous décidez comment présenter une action, trois patterns couvrent la plupart des cas :
« Vous pouvez voir mais pas modifier » est courant et mérite d'être conçu explicitement. Traitez-le comme deux permissions : une pour lire les données et une pour les modifier. Dans le menu, vous pouvez afficher Détails client pour tous ceux qui peuvent lire, mais n'afficher Modifier le client qu'à ceux qui ont l'accès en écriture. Sur la page, rendez les champs en lecture seule et protégez les contrôles d'édition, tout en laissant la page se charger.
Surtout, le backend décide de l'issue finale. Même si l'UI cache chaque action admin, le serveur doit quand même vérifier les permissions sur chaque requête sensible et renvoyer une réponse claire « non autorisé » quand quelqu'un essaie.
Le moyen le plus rapide de livrer des menus sensibles aux permissions est de commencer par un modèle que votre équipe peut expliquer en une phrase. Si vous ne pouvez pas l'expliquer, vous ne le garderez pas correct.
Utilisez les rôles pour regrouper, pas pour signifier. Admin et Support sont des bacs utiles. Mais quand les rôles se multiplient (Admin-West-Coast-ReadOnly), l'UI devient un labyrinthe et le backend devient du guessing.
Privilégiez les permissions comme source de vérité pour ce que quelqu'un peut faire. Gardez-les petites et basées sur l'action, comme invoice.create ou customer.export. Cela scale mieux que la prolifération de rôles parce que les nouvelles fonctionnalités ajoutent généralement de nouvelles actions, pas de nouveaux intitulés de poste.
Ajoutez ensuite des policies (règles) pour le contexte. C'est là que vous gérez « peut éditer seulement son propre enregistrement » ou « peut approuver des factures uniquement sous 5 000 $ ». Les policies évitent de créer des dizaines de permissions presque identiques qui ne diffèrent que par une condition.
Une couche maintenable ressemble à ceci :
Le nommage compte plus qu'on ne le croit. Si votre UI dit Exporter les clients mais que l'API utilise download_all_clients_v2, vous finirez par masquer la mauvaise chose ou bloquer la bonne. Gardez les noms humains, cohérents et partagés entre frontend et backend :
nom. verbe (ou ressource.action) de façon cohérenteExemple : dans un CRM, un rôle Sales peut inclure lead.create et lead.update, mais une policy limite les mises à jour aux leads dont l'utilisateur est propriétaire. Cela garde votre menu clair tandis que votre backend reste strict.
Les menus sensibles aux permissions font bonne impression parce qu'ils réduisent l'encombrement et évitent les clics accidentels. Mais ils n'aident que si le backend reste aux commandes. Pensez l'UI comme un indice, et le serveur comme le juge.
Commencez par écrire ce que vous protégez. Pas des pages, mais des actions. Voir la liste de clients est différent d'exporter les clients ou de supprimer un client. C'est l'épine dorsale des menus sensibles aux permissions qui ne tournent pas au théâtre de sécurité.
canEditCustomers, canDeleteCustomers, canExport, ou une liste compacte de chaînes de permission. Gardez-le minimal.Une petite règle importante : ne faites jamais confiance aux rôles ou flags fournis par le client. L'UI peut cacher des boutons en se basant sur les capabilities, mais l'API doit toujours rejeter les requêtes non autorisées.
Les menus sensibles aux permissions doivent aider les gens à trouver ce qu'ils peuvent faire, pas prétendre appliquer la sécurité. Le frontend est une glissière de guidage. Le backend est la serrure.
Au lieu d'éparpiller des vérifications de permissions sur chaque bouton, définissez votre navigation depuis une seule config qui inclut la permission requise pour chaque item, puis rendez à partir de cette config. Cela garde les règles lisibles et évite d'oublier des vérifications dans des coins bizarres de l'UI.
Un pattern simple ressemble à ceci :
const menu = [
{ label: "Contacts", path: "/contacts", requires: "contacts.read" },
{ label: "Export", action: "contacts.export", requires: "contacts.export" },
{ label: "Admin", path: "/admin", requires: "admin.access" },
];
const visibleMenu = menu.filter(item => userPerms.includes(item.requires));
Privilégiez le masquage de sections entières (comme Admin) plutôt que de parsemer des vérifications sur chaque lien de page admin. C'est moins de lieux à rater.
Masquez les éléments quand l'utilisateur ne sera jamais autorisé à les utiliser. Désactivez quand l'utilisateur a la permission, mais que le contexte actuel est insuffisant.
Exemple : Supprimer un contact doit être désactivé tant qu'aucun contact n'est sélectionné. Même permission, juste pas assez de contexte pour l'instant. Quand vous désactivez, ajoutez un court message « pourquoi » près du contrôle (tooltip, texte d'aide, ou note inline) : Sélectionnez un contact pour le supprimer.
Un ensemble de règles qui tient la route :
Masquer des éléments de menu aide les gens à se concentrer, mais ça ne protège rien. Le backend doit être le juge final parce que les requêtes peuvent être rejouées, modifiées ou déclenchées en dehors de votre UI.
Une bonne règle : chaque action qui modifie des données nécessite une vérification d'autorisation, en un seul endroit, que chaque requête traverse. Cela peut être un middleware, un wrapper de handler, ou une petite couche de policy que vous appelez au début de chaque endpoint. Choisissez une approche et tenez-vous-y, sinon vous manquerez des chemins.
Séparez l'autorisation de la validation des entrées. Décidez d'abord « est-ce que cet utilisateur est autorisé à faire ceci ? », puis validez le payload. Si vous validez d'abord, vous pouvez divulguer des détails (comme quels IDs existent) à quelqu'un qui ne devrait même pas savoir que l'action est possible.
Un pattern qui scale :
Can(user, "invoice.delete", invoice)).Utilisez des codes de statut qui aident à la fois votre frontend et vos logs :
401 Unauthorized quand l'appelant n'est pas connecté.403 Forbidden quand connecté mais pas autorisé.Faites attention à 404 Not Found comme déguisement. Cela peut être utile pour éviter de révéler l'existence d'une ressource, mais si vous l'utilisez de façon incohérente, le débogage devient pénible. Choisissez une règle cohérente par type de ressource.
Assurez-vous que la même autorisation s'exécute que l'action vienne d'un clic de bouton, d'une app mobile, d'un script, ou d'un appel API direct.
Enfin, enregistrez les tentatives refusées pour le débogage et les audits, mais protégez bien les logs. Enregistrez qui, quelle action, et quel type de ressource haut niveau. Évitez les champs sensibles, les payloads complets ou les secrets.
La plupart des bugs de permissions apparaissent quand les utilisateurs empruntent des chemins que votre menu n'envisageait pas. C'est pourquoi les menus sensibles aux permissions sont utiles, mais seulement si vous prévoyez aussi les chemins qui les contournent.
Si le menu cache Facturation pour un rôle, un utilisateur peut toujours coller une URL sauvegardée ou l'ouvrir depuis l'historique. Traitez chaque chargement de page comme une requête fraîche : récupérez les permissions actuelles de l'utilisateur, et faites en sorte que l'écran refuse de charger les données protégées quand la permission manque. Un message amical « Vous n'avez pas accès » est acceptable, mais la vraie protection est que le backend ne renvoie rien.
N'importe qui peut appeler votre API depuis les outils dev, un script, ou un autre client. Vérifiez donc les permissions sur chaque endpoint, pas seulement les écrans admin. Le risque facile à manquer est celui des actions en masse : un seul /items/bulk-update peut accidentellement permettre à un non-admin de modifier des champs qu'il ne voit jamais dans l'UI.
Les rôles peuvent aussi changer en cours de session. Si un admin retire une permission, l'utilisateur peut encore avoir un ancien token ou un menu en cache. Utilisez des tokens à courte durée de vie ou une consultation de permissions côté serveur, et gérez les réponses 401/403 en rafraîchissant les permissions et en mettant à jour l'UI.
Les appareils partagés créent un autre piège : l'état du menu en cache peut fuir entre comptes. Stockez la visibilité du menu indexée par user ID, ou évitez de la persister.
Cinq tests à exécuter avant la sortie :
Imaginez un CRM interne avec trois rôles : Sales, Support, et Admin. Tout le monde se connecte et l'app affiche un menu à gauche, mais le menu n'est qu'une commodité. La vraie sécurité est ce que le serveur autorise.
Voici un simple jeu de permissions lisible :
L'UI commence par demander au backend les actions autorisées de l'utilisateur courant (souvent sous forme d'une liste de chaînes de permission) plus du contexte basique comme l'ID utilisateur et l'équipe. Le menu se construit à partir de cela. Si vous n'avez pas billing.view, vous ne voyez pas Facturation. Si vous avez leads.export, vous voyez un bouton Exporter sur l'écran Leads. Si vous ne pouvez éditer que vos propres leads, le bouton Edit peut toujours apparaître, mais il doit être désactivé ou afficher un message clair lorsqu'un lead ne vous appartient pas.
Maintenant la partie importante : chaque endpoint d'action applique les mêmes règles.
Exemple : Sales peut créer des leads et éditer les leads dont il est propriétaire. Support peut voir les tickets et les assigner, mais ne touche pas à la facturation. Admin peut gérer les utilisateurs et la facturation.
Quand quelqu'un essaie de supprimer un lead, le backend vérifie :
leads.delete ?lead.owner_id == user.id ?Même si un utilisateur Support appelle manuellement l'endpoint delete, il reçoit une réponse forbidden. L'élément de menu caché n'était jamais la protection. La décision backend l'était.
Le plus grand piège avec les menus sensibles aux permissions est de penser que le travail est fini quand le menu a l'air correct. Masquer des boutons réduit la confusion, mais n'atténue pas le risque.
Erreurs qui surviennent le plus souvent :
isAdmin pour tout. Ça semble rapide, puis ça se propage. Bientôt chaque exception devient un cas spécial et plus personne ne peut expliquer les règles d'accès.role, isAdmin, ou permissions venant du navigateur comme vérité. Dérivez l'identité et l'accès depuis votre propre session ou token, puis cherchez rôles et permissions côté serveur.Un exemple concret : vous cachez l'item Exporter les leads pour les non-managers. Si l'endpoint d'export ne vérifie pas non plus les permissions, n'importe quel utilisateur qui devine la requête (ou la copie depuis un collègue) peut quand même télécharger le fichier.
Avant de livrer des menus sensibles aux permissions, faites une dernière passe centrée sur ce que les utilisateurs peuvent réellement faire, pas sur ce qu'ils peuvent voir.
Parcourez votre app comme chaque rôle principal et essayez le même ensemble d'actions. Faites-le dans l'UI et aussi en appelant l'endpoint directement (ou en utilisant les outils dev) pour vous assurer que le serveur est la source de vérité.
Checklist :
Un moyen pratique de repérer les lacunes : choisissez un bouton « dangereux » (supprimer un utilisateur, exporter un CSV, changer la facturation) et suivez-le de bout en bout. L'item du menu doit être masqué quand il le faut, l'API doit rejeter les appels non autorisés, et l'UI doit récupérer proprement quand elle reçoit un 403.
Commencez petit. Vous n'avez pas besoin d'une matrice d'accès parfaite le premier jour. Choisissez la poignée d'actions qui comptent le plus (voir, créer, éditer, supprimer, exporter, gérer les utilisateurs), mappez-les aux rôles que vous avez déjà, et avancez.
Avant de construire des écrans, faites une courte passe de planification qui liste des actions, pas des pages. Un item de menu comme Factures cache beaucoup d'actions : voir la liste, voir le détail, créer, rembourser, exporter. Les écrire en premier rend l'UI et les règles backend plus claires, et évite l'erreur courante qui consiste à protéger une page entière tout en laissant un endpoint risqué sous-protégé.
Quand vous refactorez des règles d'accès, traitez cela comme tout changement risqué : gardez un filet de sécurité. Les snapshots vous permettent de comparer le comportement avant/après. Si un rôle perd soudainement un accès nécessaire, ou en gagne un qu'il ne devrait pas avoir, revenir en arrière est plus rapide que de corriger en production pendant que les utilisateurs sont bloqués.
Une routine de release simple aide les équipes à avancer vite sans deviner :
Si vous construisez avec une plateforme basée chat comme Koder.ai (koder.ai), cette même structure s'applique : définissez les permissions et policies une fois, faites lire les capabilities par l'UI depuis le serveur, et rendez les vérifications backend non optionnelles dans chaque handler.
Les menus sensibles aux permissions résolvent surtout la clarté, pas la sécurité. Ils aident les utilisateurs à se concentrer sur ce qu'ils peuvent réellement faire, réduisent les clics frustrants et diminuent les questions de support du type « pourquoi je vois ceci ? ».
La sécurité doit toujours être appliquée côté backend, car n'importe qui peut tenter des deep links, utiliser d'anciens favoris ou appeler directement vos API, indépendamment de ce que l'UI affiche.
Masquez quand une fonctionnalité doit être effectivement indétectable pour un rôle et qu'il n'y a pas de chemin attendu pour y accéder.
Désactivez quand l'utilisateur pourrait avoir l'accès mais qu'il lui manque le contexte (par exemple : aucun enregistrement sélectionné, état du formulaire invalide, ou données encore en chargement). Si vous désactivez, ajoutez une courte explication pour que ça ne ressemble pas à un bug.
Parce que visibilité n'est pas autorisation. Un utilisateur peut coller une URL, réutiliser une page admin depuis ses favoris, ou appeler votre API en dehors de l'UI.
Considérez l'UI comme un guide. Considérez le backend comme le décideur final pour chaque requête sensible.
Votre serveur doit renvoyer une petite réponse « capabilities » après la connexion ou le rafraîchissement de session, basée sur des vérifications de permissions côté serveur. L'UI s'appuie ensuite là-dessus pour rendre les menus et boutons.
Ne faites pas confiance à des drapeaux fournis par le client comme isAdmin venant du navigateur ; calculez les permissions à partir de l'identité authentifiée sur le serveur.
Commencez par inventorier les actions, pas les pages. Pour chaque fonctionnalité, séparez lecture, création, mise à jour, suppression, export, invitation, et changements de facturation.
Ensuite, appliquez chaque permission dans le handler backend (ou middleware/wrapper) avant d'effectuer le travail. Branchez le menu sur les mêmes noms de permissions pour garder l'UI et l'API alignées.
Par défaut pratique : les rôles sont des bacs (buckets), les permissions sont la source de vérité. Gardez les permissions petites et orientées action (par exemple invoice.create), et attachez-les aux rôles.
Si les rôles commencent à se multiplier pour encoder des conditions (comme région ou propriété), déplacez ces conditions dans des politiques au lieu de créer des variantes infinies de rôles.
Utilisez des politiques pour les règles contextuelles comme « peut éditer uniquement son propre enregistrement » ou « peut approuver des factures en dessous d'un seuil ». Cela garde la liste de permissions stable tout en exprimant des contraintes réelles.
Le backend doit évaluer la politique en utilisant le contexte de la ressource (par exemple owner ID ou org ID), pas des suppositions de l'UI.
Pas toujours. Les lectures qui exposent des données sensibles ou qui contournent le filtrage normal doivent aussi être protégées, comme les exports, les logs d'audit, les salaires, les listes d'utilisateurs admin, ou tout endpoint qui renvoie plus que ce que l'UI montre normalement.
Une bonne base : toutes les écritures doivent être vérifiées, et les lectures sensibles doivent aussi l'être.
Les endpoints de masse sont faciles à rater car ils peuvent modifier beaucoup d'enregistrements ou de champs en une seule requête. Un utilisateur peut être bloqué dans l'UI mais quand même appeler /items/bulk-update directement.
Vérifiez les permissions pour l'action en masse elle-même, et validez aussi quels champs sont autorisés à être modifiés pour ce rôle, sinon vous risquez d'autoriser la modification de champs cachés.
Supposez que les permissions peuvent changer pendant qu'un utilisateur est connecté. Quand l'API renvoie 401 ou 403, l'UI doit le traiter comme un état normal : rafraîchir les capabilities, mettre à jour le menu et afficher un message clair.
Évitez aussi de persister la visibilité du menu de manière à ce qu'elle puisse fuir entre comptes sur des appareils partagés ; si vous la mettez en cache, indexez-la par identité utilisateur ou n'enregistrez rien du tout.