Voici comment j'ai exploité le système d'autorisation d'ASP.NET Blazor pour gérer les rôles et les droits dans ReassortPro
Stack technique : .NET 10 · Blazor Server (Interactive SSR) · ASP.NET Core Authorization · Radzen Components · Mediator · SQL Server
Introduction
Dans une application métier comme ReassortPro — un logiciel de gestion de réassort, de stock et de commandes fournisseur — la gestion fine des droits utilisateurs est un enjeu central. Chaque utilisateur possède un ou plusieurs rôles (Gestionnaire, Chef d'agence, Magasinier, Chef de projet, Opérateur), et chaque action de l'interface (afficher les produits, voir les prix d'achat, créer une commande, supprimer un inventaire…) doit être conditionnée par une politique d'autorisation configurable à chaud, sans redéploiement.
Plutôt que de réinventer la roue, j'ai décidé de m'appuyer entièrement sur le système d'autorisation natif d'ASP.NET Core — IAuthorizationHandler, IAuthorizationRequirement, AuthorizationPolicy — en l'étendant pour répondre à un besoin spécifique : une matrice Rôles × Politiques entièrement pilotée par la base de données.
Cet article détaille l'approche théorique, l'architecture retenue, puis l'implémentation concrète dans ReassortPro.
1. Les fondamentaux de l'autorisation dans ASP.NET Core
1.1 Authentification vs Autorisation
flowchart LR
A[Requête HTTP] --> B{Authentifié ?}
B -->|Non| C[Redirection vers /connexion]
B -->|Oui| D{Autorisé ?}
D -->|Non| E[403 — Accès refusé]
D -->|Oui| F[Accès à la ressource]- Authentification : « Qui êtes-vous ? » — vérifier l'identité de l'utilisateur (cookie, token JWT, etc.).
- Autorisation : « Avez-vous le droit ? » — vérifier que l'utilisateur authentifié possède les permissions nécessaires.
ASP.NET Core sépare clairement ces deux responsabilités. L'authentification est gérée par un AuthenticationHandler (ici, Cookie Authentication), tandis que l'autorisation repose sur un pipeline indépendant.
1.2 Les trois piliers de l'autorisation ASP.NET Core
classDiagram
class IAuthorizationRequirement {
<<interface>>
}
class AuthorizationHandler~TRequirement~ {
<<abstract>>
+HandleRequirementAsync(context, requirement)
}
class AuthorizationPolicy {
+string Name
+IList~IAuthorizationRequirement~ Requirements
}
class AuthorizationOptions {
+AddPolicy(name, policy)
}
AuthorizationPolicy --> IAuthorizationRequirement : contient 1..*
AuthorizationHandler~TRequirement~ --> IAuthorizationRequirement : évalue
AuthorizationOptions --> AuthorizationPolicy : enregistre| Concept | Rôle | Analogie |
|---|---|---|
IAuthorizationRequirement |
Décrit ce qui est requis pour qu'un accès soit autorisé | La question posée |
AuthorizationHandler<T> |
Contient la logique de décision pour évaluer un requirement | Le juge qui répond |
AuthorizationPolicy |
Regroupe un ou plusieurs requirements sous un nom | La règle nommée |
1.3 Cycle de vie d'une vérification d'autorisation
sequenceDiagram
participant Page as Page Blazor
participant MW as Middleware Authorization
participant AZ as AuthorizationService
participant H as AuthorizationHandler
participant DB as Base de données (cache)
Page->>MW: @attribute [Authorize(Policy = "CanDisplayProduct")]
MW->>AZ: Évaluer la politique "CanDisplayProduct"
AZ->>AZ: Résoudre les Requirements de la politique
AZ->>H: HandleRequirementAsync(context, requirement)
H->>DB: Vérifier les rôles autorisés pour ce PolicyId
DB-->>H: Liste des rôles autorisés
H->>H: Comparer avec les rôles du membre connecté
alt Autorisé
H-->>AZ: context.Succeed(requirement)
AZ-->>MW: Autorisation accordée
MW-->>Page: Afficher le contenu
else Refusé
H-->>AZ: context.Fail()
AZ-->>MW: Autorisation refusée
MW-->>Page: Rediriger vers /autorisation
end1.4 Policy-based vs Role-based : pourquoi les politiques ?
L'approche classique [Authorize(Roles = "Admin")] est rigide : les rôles sont codés en dur. Si demain on veut qu'un « Magasinier » puisse aussi supprimer un produit, il faut modifier le code et redéployer.
Avec les politiques (policy-based authorization), la logique est découplée :
// Rigide : le rôle est dans le code
[Authorize(Roles = "Admin,StoreKeeper")]
// Flexible : la politique est un nom, sa logique est configurable
[Authorize(Policy = "CanDeleteProduct")]
La politique CanDeleteProduct peut être associée dynamiquement à n'importe quel rôle, sans toucher au code.
2. Architecture de l'autorisation dans ReassortPro
2.1 Vue d'ensemble
graph TB
subgraph "Couche Présentation (Blazor)"
A["@attribute [Authorize(Policy)]"]
B["<AuthorizeView Policy>"]
C[PolicyButton]
D[PolicyPanelMenuItem]
E[ListPolicy — Matrice de configuration]
end
subgraph "Couche Services"
F[PolicyService]
G[AsyncAccessHandler]
H[MemberAuthenticator]
end
subgraph "Couche Domaine"
I["Policy (SuperEnum)"]
J["MemberRole (SuperEnum)"]
K["PolicyGroup (SuperEnum)"]
L[RoleListByPolicy]
M[Member]
end
subgraph "Infrastructure"
N[ICacheService]
O[SQL Server]
end
A --> G
B --> G
C --> B
D --> B
G --> H
G --> F
F --> N
N --> O
H --> M
F --> I
F --> L
I --> K
M --> J
E --> F2.2 Les SuperEnums : un pattern maison
ReassortPro utilise un pattern de SuperEnum — des record C# dont les instances statiques jouent le rôle de valeurs d'énumération, tout en portant des métadonnées riches (Id, nom d'affichage, groupe, position).
classDiagram
class Policy {
+Guid Id
+string Name
+string DisplayName
+PolicyGroup Group
+int Position
+GetValues() IEnumerable~Policy~
+GetById(Guid) Policy
+GetByName(string) Policy
}
class PolicyGroup {
+string Name
+int Position
}
class MemberRole {
+Guid Id
+string Name
+string DisplayName
+string Description
+int Level
+GetValues() IEnumerable~MemberRole~
}
class RoleListByPolicy {
+Guid Id
+Guid PolicyId
+List~Guid~ RoleList
+DateTime CreationDate
+DateTime LastUpdate
}
Policy --> PolicyGroup : appartient à
RoleListByPolicy --> Policy : référence
RoleListByPolicy --> MemberRole : contient des Ids de3. Implémentation détaillée
3.1 Définir les politiques — le SuperEnum Policy
Chaque droit est défini comme une propriété statique du record Policy. Chaque politique est identifiée par un Guid stable (jamais régénéré), un nom technique et un nom d'affichage en français.
public record Policy(Guid Id, string Name, string DisplayName, PolicyGroup Group, int Position)
{
public static Policy CanDisplayProduct => new(
new Guid("eae6cec4-06d0-4e65-9894-f8843e97f6e5"),
nameof(CanDisplayProduct),
"Afficher les produits",
PolicyGroup.Catalog, 1);
public static Policy CanAddProduct => new(
new Guid("7cd50a7b-b7bb-46a7-b49e-2ccc88bee671"),
nameof(CanAddProduct),
"Ajouter un produit",
PolicyGroup.Catalog, 2);
public static Policy CanDeleteProduct => new(
new Guid("2a525d06-0d7d-44d9-bd14-1288ad9be77a"),
nameof(CanDeleteProduct),
"Supprimer un produit",
PolicyGroup.Catalog, 3);
// ... 40+ politiques organisées par groupe
}
Les politiques sont organisées en groupes fonctionnels :
public record PolicyGroup(string Name, int Position)
{
public static PolicyGroup Catalog => new("Catalogue", 0);
public static PolicyGroup Storage => new("Stock", 1);
public static PolicyGroup Orders => new("Commandes", 2);
public static PolicyGroup Thirds => new("Tiers", 3);
public static PolicyGroup UI => new("Interface", 4);
}
3.2 Les rôles métier — le SuperEnum MemberRole
Les rôles sont définis de la même manière :
public record MemberRole(Guid Id, string Name, string DisplayName, string Description, int Level)
{
public static MemberRole CustomerAdmin => new(
new Guid("b07f66ef-ac4a-4d3c-b45e-058e7be24803"),
nameof(CustomerAdmin), "Gestionnaire",
"Gestionnaire, peut tout faire", 0);
public static MemberRole AgencyManager => new(
new Guid("fce906df-fbab-47a9-bd39-3d9606769ce2"),
nameof(AgencyManager), "Chef d'agence",
"Peut gerer les données d'une agence", 1);
public static MemberRole StoreKeeper => new(
new Guid("8f254759-475e-462a-9f4c-dc1a75a94031"),
nameof(StoreKeeper), "Magasinier",
"Gère le stock et les fournisseurs", 2);
public static MemberRole ProjectManager => new(
new Guid("9c5f20f2-4d3d-4b00-ba25-9e9da3a02490"),
nameof(ProjectManager), "Chef de projet",
"Peut passer des commandes fournisseur", 2);
public static MemberRole Operator => new(
new Guid("c203217b-3ab5-4422-9e1b-883a59254197"),
nameof(Operator), "Opérateur",
"Peut gerer le stock de produit", 3);
}
Et chaque Member porte une liste de Guids de rôles :
public class Member
{
public Guid Id { get; set; }
public string Name { get; set; } = null!;
public string Email { get; set; } = null!;
public List<Guid> RoleList { get; set; } = new();
// ...
}
3.3 La table de liaison — RoleListByPolicy
La correspondance entre politiques et rôles autorisés est stockée en base de données :
public class RoleListByPolicy
{
public Guid Id { get; set; }
public Guid PolicyId { get; set; }
public List<Guid> RoleList { get; set; } = new();
public DateTime CreationDate { get; set; }
public DateTime LastUpdate { get; set; }
}
Cela signifie qu'on peut, à chaud, décider que la politique CanDeleteProduct est accessible aux rôles CustomerAdmin et StoreKeeper, sans jamais modifier le code.
erDiagram
MEMBER {
guid Id PK
string Name
string Email
bool IsSuperAdmin
}
MEMBER_ROLES {
guid MemberId FK
guid RoleId FK
}
ROLE_LIST_BY_POLICY {
guid Id PK
guid PolicyId
datetime CreationDate
datetime LastUpdate
}
ROLE_LIST_BY_POLICY_ROLES {
guid RoleListByPolicyId FK
guid RoleId FK
}
MEMBER ||--o{ MEMBER_ROLES : "a les rôles"
ROLE_LIST_BY_POLICY ||--o{ ROLE_LIST_BY_POLICY_ROLES : "autorise les rôles"3.4 Le Requirement — AsyncAccessRequirement
Le requirement est volontairement minimaliste. Il ne porte qu'un PolicyId :
public class AsyncAccessRequirement : IAuthorizationRequirement
{
public Guid PolicyId { get; set; }
}
Toute la logique de décision est déléguée au handler.
3.5 Le Handler — AsyncAccessHandler
C'est le cœur du système. Le handler est enregistré comme Singleton et utilise l'injection de dépendances :
public class AsyncAccessHandler(
PolicyService policyService,
MemberAuthenticator memberAuthenticator)
: AuthorizationHandler<AsyncAccessRequirement>
{
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AsyncAccessRequirement requirement)
{
// 1. Récupérer le membre connecté
var cm = await memberAuthenticator.GetConnectedMember();
if (cm is null)
{
context.Fail();
return;
}
// 2. Le SuperAdmin a tous les droits
if (cm.Member.IsSuperAdmin)
{
context.Succeed(requirement);
return;
}
// 3. Vérifier la matrice rôle ↔ politique
var isAllowed = await policyService.IsAllowed(requirement.PolicyId, cm);
if (isAllowed)
{
context.Succeed(requirement);
return;
}
context.Fail();
}
}
flowchart TD
A[HandleRequirementAsync] --> B{Membre connecté ?}
B -->|null| C["context.Fail()"]
B -->|Oui| D{IsSuperAdmin ?}
D -->|Oui| E["context.Succeed()"]
D -->|Non| F{Policy = SuperAdmin ?}
F -->|Oui| C
F -->|Non| G["policyService.IsAllowed()"]
G -->|true| E
G -->|false| C3.6 Le Service de politiques — PolicyService
Le PolicyService est responsable de :
- L'enregistrement statique de toutes les politiques au démarrage
- L'évaluation dynamique des droits à l'exécution (avec cache)
Enregistrement des politiques
public class PolicyService(IMediator mediator, ICacheService cache)
{
public const string SuperAdmin = "SuperAdmin";
public const string NoPolicy = "NoPolicy";
public static void AddPolicies(AuthorizationOptions options)
{
// Politique spéciale SuperAdmin
AddSuperAdminPolicy(options);
// Politique "ouverte" sans restriction
AddNoPolicy(options);
// Toutes les politiques métier
foreach (var policy in Policy.GetValues())
{
options.AddPolicy(policy.Name, p =>
{
p.Requirements.Add(new AsyncAccessRequirement
{
PolicyId = policy.Id
});
});
}
}
internal static void AddSuperAdminPolicy(AuthorizationOptions options)
{
options.AddPolicy(SuperAdmin, p =>
p.Requirements.Add(new AsyncAccessRequirement
{
PolicyId = 1.ToGuid()
}));
}
internal static void AddNoPolicy(AuthorizationOptions options)
{
options.AddPolicy(NoPolicy, cfg =>
cfg.RequireAssertion(ctx => true));
}
}
Point clé :
AddPoliciesest appelé une seule fois au démarrage. Il itère sur toutes les valeurs du SuperEnumPolicyet crée une politique ASP.NET Core pour chacune. Cela garantit que tout@attribute [Authorize(Policy = "CanDisplayProduct")]dans un composant Blazor est résolu par le framework.
Évaluation dynamique avec cache
public async Task<bool> IsAllowed(Guid policyId, ConnectedMember connectedMember)
{
if (_loading)
{
return false;
}
// Tenter de lire la matrice depuis le cache
cache.TryGetValue("allrolebypolicy", out List<RoleListByPolicy>? allRoleByPolicy);
if (allRoleByPolicy is null)
{
_loading = true;
try
{
// Charger depuis la base via Mediator
allRoleByPolicy = await mediator.Send(new GetAllRoleByPolicyRequest());
cache.AddWithNeverRemove("allrolebypolicy", allRoleByPolicy);
}
finally
{
_loading = false;
}
}
// Trouver la config pour cette politique
var rbp = allRoleByPolicy.FirstOrDefault(r => r.PolicyId == policyId);
if (rbp is null)
{
// Pas encore configuré → tout est autorisé par défaut
return true;
}
if (!rbp.RoleList.Any())
{
return false;
}
// Vérifier l'intersection entre les rôles du membre et les rôles autorisés
return connectedMember.Member.IsInRoles(rbp.RoleList);
}
flowchart TD
A["IsAllowed(policyId, member)"] --> B{Cache disponible ?}
B -->|Non| C[Charger depuis la BDD via Mediator]
C --> D[Stocker en cache permanent]
D --> E{Config trouvée pour cette politique ?}
B -->|Oui| E
E -->|null| F["return true (tout autorisé par défaut)"]
E -->|Trouvée| G{Des rôles sont-ils définis ?}
G -->|Aucun| H["return false"]
G -->|Oui| I{Le membre a-t-il un rôle autorisé ?}
I -->|Oui| J["return true"]
I -->|Non| HStratégie du "tout autorisé par défaut" : quand une politique n'a pas encore été configurée en base (pas de
RoleListByPolicy), tous les rôles sont autorisés. Cela permet un déploiement progressif des restrictions sans bloquer les utilisateurs existants.
3.8 Enregistrement dans Program.cs
Tout le câblage se fait au démarrage :
// 1. Authentification par cookie
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
// ...
options.AccessDeniedPath = "/autorisation";
options.LoginPath = "/connexion";
});
// 2. Services d'autorisation
builder.Services.AddSingleton<PolicyService>();
builder.Services.AddSingleton<IAuthorizationHandler, AsyncAccessHandler>();
builder.Services.AddAuthorization(options =>
{
PolicyService.AddPolicies(options);
});
// 3. Authenticator
builder.Services.AddTransient<MemberAuthenticator>();
4. Utilisation dans l'interface Blazor
4.1 Protection d'une page entière
Pour protéger une page, il suffit d'ajouter l'attribut [Authorize] avec le nom de la politique :
@page "/commande/{id:guid}"
@attribute [Authorize(Policy = nameof(Policy.CanDisplaySupplierOrder))]
<h1>Détail de la commande fournisseur</h1>
L'utilisation de nameof() garantit la sécurité à la compilation : si la politique est renommée ou supprimée, le code ne compile plus.
4.2 Affichage conditionnel avec <AuthorizeView>
Pour masquer un élément selon les droits, Blazor fournit le composant <AuthorizeView> :
<AuthorizeView Policy="CanDeleteProduct">
<Authorized>
<button @onclick="DeleteProduct">Supprimer</button>
</Authorized>
</AuthorizeView>
4.3 Les composants Policy* — abstraction réutilisable
Pour éviter de répéter le pattern <AuthorizeView> partout, ReassortPro définit des composants wrapper qui encapsulent la vérification d'autorisation.
PolicyButton — Bouton conditionnel
@code {
[Parameter]
public Policy Policy { get; set; } = default!;
[Parameter]
public string? PolicyName { get; set; }
protected override void OnInitialized()
{
if (Policy is not null)
{
PolicyName = Policy.Name;
}
if (PolicyName is null)
{
PolicyName = PolicyService.NoPolicy;
}
}
}
<AuthorizeView Policy="@PolicyName">
<Authorized>
<SuperButton Click="@Click"
Icon="@Icon"
Text="@Text"
ButtonStyle="@this.ButtonStyle"
... />
</Authorized>
</AuthorizeView>
Utilisation :
<PolicyButton Policy="@Policy.CanAddProduct"
Text="Ajouter un produit"
Icon="add"
Click="@AddProduct" />
Le bouton n'apparaît que si l'utilisateur a le droit CanAddProduct. Pas de if/else, pas de code impératif.
PolicyPanelMenuItem — Élément de menu conditionnel
Le même principe est appliqué à la navigation latérale :
@code {
[Parameter]
public Policy Policy { get; set; } = default!;
protected override void OnInitialized()
{
if (Policy is not null)
{
PolicyName = Policy.Name;
}
if (PolicyName is null)
{
PolicyName = PolicyService.NoPolicy;
}
}
}
<AuthorizeView Policy="@PolicyName">
<Authorized>
<MenuItem Icon="@this.Icon"
Path="@this.Path"
Text="@this.Text"
Match="@this.Match" />
</Authorized>
</AuthorizeView>
Utilisation dans le menu latéral :
<PanelMenu>
<PolicyPanelMenuItem Policy="@Policy.CanDisplayDashboard"
Text="Tableau de bord" Icon="analytics" Path="/" />
<PolicyPanelMenuItem Policy="@Policy.CanDisplayProduct"
Text="Les Produits" Icon="handyman" Path="/produits" />
<PolicyPanelMenuItem Policy="@Policy.CanDisplaySupplierOrder"
Text="Les Commandes" Icon="shopping_cart" Path="/commandes" />
<PolicyPanelMenuItem Policy="@Policy.CanDisplayConfiguration"
Text="Configuration" Icon="settings">
<ChildContent>
<PolicyPanelMenuItem Policy="@Policy.CanDisplaySupplier"
Text="Les Fournisseurs" Icon="store" Path="/fournisseurs" />
<PolicyPanelMenuItem Policy="@Policy.CanManageMember"
Text="Les Utilisateurs" Icon="manage_accounts"
Path="/utilisateurs" />
</ChildContent>
</PolicyPanelMenuItem>
</PanelMenu>
Le résultat : chaque utilisateur voit un menu différent selon ses droits, sans une seule ligne de logique conditionnelle dans le composant page.
graph LR
subgraph "Gestionnaire"
A1[Tableau de bord]
A2[Produits]
A3[Commandes]
A4[Configuration]
A5[Utilisateurs]
end
subgraph "Magasinier"
B1[Tableau de bord]
B2[Produits]
B3[Inventaires]
B4[Mouvements]
end
subgraph "Opérateur"
C1[Pickings]
C2[Réceptions]
end5. La matrice de configuration — l'interface d'administration
5.1 La page /droits
La page ListPolicy affiche une matrice avec toutes les politiques en lignes et tous les rôles en colonnes. Un administrateur peut activer ou désactiver chaque intersection en un clic :
6. Flux complet — de la page à la base de données
sequenceDiagram
participant Admin as Administrateur
participant LP as ListPolicy.razor
participant Med as MediatR
participant DB as SQL Server
participant Cache as ICacheService
Note over Admin,Cache: Phase 1 — Configuration des droits
Admin->>LP: Décoche "Opérateur" pour "Supprimer un produit"
LP->>Med: SaveAllRoleByPolicyRequest
Med->>DB: UPDATE RoleListByPolicy
DB-->>Med: OK
Med-->>LP: CommandResult.Success
LP->>Cache: Invalider "allrolebypolicy"
Note over Admin,Cache: Phase 2 — Un Opérateur accède à la page
participant Op as Opérateur
participant Page as Page Produit
participant AH as AsyncAccessHandler
participant PS as PolicyService
participant MA as MemberAuthenticator
Op->>Page: Naviguer vers /produit/xxx
Page->>AH: Évaluer [Authorize(Policy = "CanDeleteProduct")]
AH->>MA: GetConnectedMember()
MA-->>AH: ConnectedMember (rôle = Operator)
AH->>PS: IsAllowed(PolicyId, member)
PS->>Cache: TryGetValue("allrolebypolicy")
Cache-->>PS: null (invalidé)
PS->>Med: GetAllRoleByPolicyRequest
Med->>DB: SELECT * FROM RoleListByPolicy
DB-->>Med: Données
Med-->>PS: List~RoleListByPolicy~
PS->>Cache: Stocker en cache permanent
PS->>PS: Intersect(member.RoleList, policy.RoleList)
PS-->>AH: false
AH-->>Page: context.Fail()
Page-->>Op: Le bouton Supprimer est masqué7. Avantages de cette architecture
✅ Configuration à chaud
Les droits sont modifiables par un administrateur sans redéploiement. Le cache est invalidé, et le prochain appel recharge la matrice.
✅ Sécurité en profondeur
La protection opère à deux niveaux :
- Page :
@attribute [Authorize(Policy = ...)]— empêche l'accès via l'URL - Composant :
<AuthorizeView>/PolicyButton— masque les contrôles dans l'UI
✅ Typage fort
L'utilisation de nameof(Policy.CanDisplayProduct) garantit une vérification à la compilation. Impossible de référencer une politique qui n'existe pas.
✅ Extensibilité
Ajouter une nouvelle politique = ajouter une propriété statique dans le record Policy. Le système la découvre automatiquement via la réflexion et l'enregistre dans ASP.NET Core.
✅ Maintenabilité
Les composants PolicyButton et PolicyPanelMenuItem encapsulent la logique d'autorisation. Les développeurs n'ont jamais à écrire de if (user.IsInRole(...)) dans les pages.
✅ SuperAdmin bypass
Les utilisateurs SuperAdmin ont automatiquement accès à tout, sans configuration. Ce court-circuit est géré à un seul endroit dans le handler.
8. Récapitulatif des classes et leurs responsabilités
| Classe | Responsabilité |
|---|---|
Policy |
SuperEnum des 40+ politiques métier |
PolicyGroup |
Groupes fonctionnels (Catalogue, Stock, Commandes…) |
MemberRole |
SuperEnum des rôles utilisateur |
RoleListByPolicy |
Table de liaison rôles ↔ politique (BDD) |
Member |
Entité utilisateur avec RoleList et IsSuperAdmin |
AsyncAccessRequirement |
Requirement ASP.NET Core portant un PolicyId |
AsyncAccessHandler |
Handler évaluant le requirement via le PolicyService |
PolicyService |
Enregistrement des politiques + évaluation dynamique |
MemberAuthenticator |
Résolution de l'identité cookie → ConnectedMember |
PolicyButton |
Bouton Radzen conditionnel par politique |
PolicyPanelMenuItem |
Élément de menu conditionnel par politique |
PolicySplitButtonItem |
Élément de menu dropdown conditionnel |
MobilePolicyButton |
Bouton mobile conditionnel par politique |
ListPolicy |
Interface d'administration de la matrice rôles × politiques |
Conclusion
Le système d'autorisation d'ASP.NET Core est bien plus qu'un simple [Authorize(Roles = "Admin")]. En exploitant le trio Requirement / Handler / Policy, j'ai pu construire dans ReassortPro un système de gestion des droits :
- Déclaratif dans les composants Blazor (
nameof(Policy.CanDeleteProduct)) - Dynamique en base de données (matrice rôles × politiques)
- Performant grâce au cache (un seul chargement,
FrozenSetpour les valeurs statiques) - Extensible par simple ajout d'une propriété dans un record
Le pattern des SuperEnums apporte la rigueur du typage fort tout en conservant la flexibilité d'un catalogue découvrable par réflexion. Et les composants wrapper (PolicyButton, PolicyPanelMenuItem) rendent l'autorisation invisible pour les développeurs qui construisent les pages — il leur suffit de passer la bonne Policy en paramètre.
Ce design s'est révélé extrêmement robuste en production : plus de 40 politiques, 5 rôles, des dizaines de pages et de boutons conditionnels, le tout configurable par un administrateur métier sans aucune intervention technique.