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 CoreIAuthorizationHandler, 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
    end

1.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["&lt;AuthorizeView Policy&gt;"]
        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 --> F

2.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 de

3. 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| C

3.6 Le Service de politiques — PolicyService

Le PolicyService est responsable de :

  1. L'enregistrement statique de toutes les politiques au démarrage
  2. 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é : AddPolicies est appelé une seule fois au démarrage. Il itère sur toutes les valeurs du SuperEnum Policy et 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| H

Straté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]
    end

5. 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 :

Image


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, FrozenSet pour 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.