Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close
    Deep Dive.NETPart 5 of 12

    Handling Authorization and Role-Based Access Control (RBAC)

    Implement secure, permission-based authorization in Clean Architecture with custom attributes, dynamic policy providers, and JWT integration.

    Introduction

    Static role checks ([Authorize(Roles = "Admin")]) fall apart the first time someone asks you to add a permission without a redeploy. Once roles and permissions have to change at runtime, hard-coded role attributes become a liability. The Contact Management Application takes a different route: a dynamic policy provider that builds authorization policies from the database at request time, covering both the backend API and the Angular frontend, wired into JWT authentication without breaking the separation of concerns Clean Architecture expects.

    System Architecture Overview

    Before diving into implementation details, let’s understand the overall RBAC architecture with this diagram:

    Frontend (Angular)Backend API (.NET)DatabaseIn order, after loginThe authorization pipelineLogin componentAuth state servicePermission servicePermission guardblocks the route*hasPermission directiveremoves the elementUsers controller[AuthorizePermission]Custom policy providerPermission handlerUsersRolesUserRolesPagesOperationsPermissionsRolePermissionsJWT + RolePermissionsvalidate against DB
    Two arrows cross a tier, and they are the whole design. Login hands the permission set to the browser once; every API call re-derives it from RolePermissions. The browser copy decides what to show; the database decides what is allowed.

    Part of the Series

    This article is part of a series covering the Contact Management Application, a sample project demonstrating Clean Architecture principles. Other articles include:

    1. Clean Architecture: Introduction to the Project Structure

    2. Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging

    3. Clean Architecture: Validating Inputs with FluentValidation

    4. Clean Architecture: Dependency Injection Setup Across Layers

    5. Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) (You are here)

    6. Clean Architecture: Implementing Activity Logging with Custom Attributes

    7. Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions

    8. Clean Architecture: Using Dapper for Data Access and Repository Pattern

    9. Clean Architecture: Best Practices for Creating and Using DTOs in the API

    10. Clean Architecture: Error Handling and Exception Management in the API

    11. Clean Architecture: Dockerizing the .NET Core API, Angular and MS SQL Server

    12. Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts


    1. RBAC Database Design

    1.1 Entity Relationship Diagram

    The RBAC system uses six interconnected entities. Here’s how they relate to each other:

    erDiagram
    User ||--o{ UserRole : "has"
    Role ||--o{ UserRole : "assigned to"
    Role ||--o{ RolePermission : "has"
    Permission ||--o{ RolePermission : "granted via"
    Page ||--o{ Permission : "has"
    Operation ||--o{ Permission : "has"
    User {
    uuid Id PK
    string FirstName
    string LastName
    string Username
    string Email
    string Password
    }
    Role {
    uuid Id PK
    string Name
    string Description
    }
    UserRole {
    uuid Id PK
    uuid UserId FK
    uuid RoleId FK
    }
    Page {
    uuid Id PK
    string Name
    string Url
    int PageOrder
    }
    Operation {
    uuid Id PK
    string Name
    string Description
    }
    Permission {
    uuid Id PK
    uuid PageId FK
    uuid OperationId FK
    string Description
    }
    RolePermission {
    uuid Id PK
    uuid RoleId FK
    uuid PermissionId FK
    }

    1.2 Core Entities

    Let’s examine each entity and its role in the RBAC system:

    Page Entity

    Represents a resource, feature, or section in the application (e.g., Contacts, Users, Roles). Each page represents something that can be protected with permissions.

    Contact.Domain/Entities/Page.cs
    public class Page : BaseEntity
    {
    public required string Name { get; set; }
    public required string Url { get; set; }
    public int PageOrder { get; set; }
    }

    Example Data:

    IdNameUrlPageOrder
    guid-1Contacts/contact1
    guid-2Users/user2
    guid-3Roles/role3

    Operation Entity

    Defines the actions that can be performed on any page. These are reusable across all pages.

    Contact.Domain/Entities/Operation.cs
    public class Operation : BaseEntity
    {
    public required string Name { get; set; }
    public required string Description { get; set; }
    }

    Example Data:

    IdNameDescription
    op-1ReadAllows reading/viewing data
    op-2CreateAllows creating new records
    op-3UpdateAllows modifying existing records
    op-4DeleteAllows removing records

    Permission Entity

    Connects a Page to an Operation, creating specific permissions like “Contacts.Read” or “Users.Delete”.

    Contact.Domain/Entities/Permission.cs
    public class Permission : BaseEntity
    {
    public Guid PageId { get; set; }
    public Guid OperationId { get; set; }
    public required string Description { get; set; }
    }

    Example Data:

    IdPageIdOperationIdDescription
    perm-1ContactsReadCan view contacts
    perm-2ContactsCreateCan create contacts
    perm-3ContactsUpdateCan update contacts
    perm-4ContactsDeleteCan delete contacts

    Role Entity

    Defines user roles such as Admin, Manager, or User. Each role is a collection of permissions.

    Contact.Domain/Entities/Role.cs
    public class Role : BaseEntity
    {
    public required string Name { get; set; }
    public required string Description { get; set; }
    }

    Example Data:

    IdNameDescription
    role-1AdminFull system access
    role-2ManagerCan manage contacts and users
    role-3UserRead-only access

    RolePermission Entity

    Maps roles to their permissions. This is the key table that determines what each role can do.

    Contact.Domain/Entities/RolePermission.cs
    public class RolePermission : BaseEntity
    {
    public Guid RoleId { get; set; }
    public Guid PermissionId { get; set; }
    }

    Example Data:

    IdRoleIdPermissionId(Result)
    rp-1AdminContacts.ReadAdmin can read contacts
    rp-2AdminContacts.CreateAdmin can create contacts
    rp-3AdminContacts.UpdateAdmin can update contacts
    rp-4AdminContacts.DeleteAdmin can delete contacts
    rp-5UserContacts.ReadUser can only read contacts

    UserRole Entity

    Assigns roles to users. A user can have multiple roles.

    Contact.Domain/Entities/UserRole.cs
    public class UserRole : BaseEntity
    {
    public Guid UserId { get; set; }
    public Guid RoleId { get; set; }
    }

    Example Data:

    IdUserIdRoleId
    ur-1alice-idAdmin
    ur-2bob-idUser
    ur-3bob-idManager

    2. Backend Authorization Infrastructure

    2.1 How Permission Checking Works

    When an API endpoint is called, here’s the complete flow:

    sequenceDiagram
    participant Client
    participant API as API Endpoint
    participant Attr as [AuthorizePermission]
    participant Provider as CustomPolicyProvider
    participant Handler as PermissionHandler
    participant DB as Database
    Client->>API: HTTP Request with JWT
    API->>Attr: Check [AuthorizePermission("Contacts.Create")]
    Attr->>Provider: Get policy "Contacts.CreatePolicy"
    Provider->>Provider: Create policy with PermissionRequirement
    Provider->>Handler: Evaluate requirement
    Handler->>Handler: Extract user roles from JWT claims
    Handler->>DB: Query RolePermission mappings
    DB-->>Handler: Return role permissions
    Handler->>Handler: Check if user's role has permission
    alt Permission Granted
    Handler-->>API: Success
    API-->>Client: 200 OK + Response
    else Permission Denied
    Handler-->>API: Forbidden
    API-->>Client: 403 Forbidden
    end

    2.2 The AuthorizePermission Attribute

    This custom attribute provides a clean syntax for protecting endpoints. Instead of writing complex policy names, you simply specify the permission in Page.Operation format.

    Contact.Api/Core/Authorization/AuthorizePermissionAttribute.cs
    using Microsoft.AspNetCore.Authorization;
    namespace Contact.Api.Core.Authorization;
    public class AuthorizePermissionAttribute : AuthorizeAttribute
    {
    public AuthorizePermissionAttribute(string permission)
    {
    // Converts "Contacts.Create" to "Contacts.CreatePolicy"
    Policy = $"{permission}Policy";
    }
    }

    Usage Example:

    Contact.Api/Controllers/ContactPersonController.cs
    [HttpPost]
    [ActivityLog("Creating new Contact")]
    [AuthorizePermission("Contacts.Create")]
    public async Task<IActionResult> Add(CreateContactPerson createContactPerson)
    {
    var createdContactPerson = await _contactPersonService.Add(createContactPerson);
    return CreatedAtAction(nameof(GetById), new { id = createdContactPerson.Id }, createdContactPerson);
    }
    [HttpPut("{id}")]
    [ActivityLog("Updating existing Contact")]
    [AuthorizePermission("Contacts.Update")]
    public async Task<IActionResult> Update(Guid id, UpdateContactPerson updateContactPerson)
    {
    await _contactPersonService.Update(id, updateContactPerson);
    return NoContent();
    }
    [HttpDelete("{id}")]
    [ActivityLog("Deleting existing Contact")]
    [AuthorizePermission("Contacts.Delete")]
    public async Task<IActionResult> Delete(Guid id)
    {
    await _contactPersonService.Delete(id);
    return NoContent();
    }
    [HttpGet]
    [AuthorizePermission("Contacts.Read")]
    public async Task<IActionResult> Get()
    {
    var contactPersons = await _contactPersonService.GetAll();
    return Ok(contactPersons);
    }

    2.3 The Permission Requirement

    A simple class that holds the permission string for the authorization requirement:

    Contact.Api/Core/Authorization/PermissionRequirement.cs
    using Microsoft.AspNetCore.Authorization;
    namespace Contact.Api.Core.Authorization;
    public class PermissionRequirement : IAuthorizationRequirement
    {
    public string Permission { get; }
    public PermissionRequirement(string permission)
    {
    Permission = permission;
    }
    }

    2.4 The Custom Authorization Policy Provider

    This is the key component that makes dynamic permissions possible. Instead of registering all policies at startup (which would require restarting the app when permissions change), this provider creates policies on-demand at runtime.

    Contact.Api/Core/Authorization/CustomAuthorizationPolicyProvider.cs
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.Extensions.Options;
    namespace Contact.Api.Core.Authorization;
    public class CustomAuthorizationPolicyProvider : IAuthorizationPolicyProvider
    {
    private readonly DefaultAuthorizationPolicyProvider _fallbackPolicyProvider;
    public CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
    {
    _fallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
    }
    public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
    {
    // Check if this is a permission-based policy (ends with "Policy")
    if (policyName.EndsWith("Policy"))
    {
    // Dynamically create a policy with PermissionRequirement
    var policy = new AuthorizationPolicyBuilder()
    .AddRequirements(new PermissionRequirement(policyName))
    .Build();
    return Task.FromResult<AuthorizationPolicy?>(policy);
    }
    // Fall back to default policy provider for non-permission policies
    return _fallbackPolicyProvider.GetPolicyAsync(policyName);
    }
    public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
    {
    return _fallbackPolicyProvider.GetDefaultPolicyAsync();
    }
    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
    return _fallbackPolicyProvider.GetFallbackPolicyAsync();
    }
    }

    Why Dynamic Policy Provider?

    The traditional approach requires registering all policies at startup:

    // Traditional approach - requires app restart for new permissions
    services.AddAuthorization(options =>
    {
    options.AddPolicy("Contacts.ReadPolicy", policy => ...);
    options.AddPolicy("Contacts.CreatePolicy", policy => ...);
    // Must add every policy here!
    });

    With CustomAuthorizationPolicyProvider:

    // Dynamic approach - policies created at runtime
    // Just register the provider, policies are created on-demand
    services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();

    2.5 The Permission Handler

    This is where the actual permission validation happens. It checks if the authenticated user’s roles have the required permission.

    Contact.Api/Core/Authorization/PermissionHandler.cs
    using Contact.Application.Interfaces;
    using Microsoft.AspNetCore.Authorization;
    namespace Contact.Api.Core.Authorization;
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
    private readonly IServiceProvider _serviceProvider;
    public PermissionHandler(IServiceProvider serviceProvider)
    {
    _serviceProvider = serviceProvider;
    }
    protected override async Task HandleRequirementAsync(
    AuthorizationHandlerContext context,
    PermissionRequirement requirement)
    {
    // Create a scope to resolve scoped services
    using var scope = _serviceProvider.CreateScope();
    var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
    var rolePermissionService = scope.ServiceProvider.GetRequiredService<IRolePermissionService>();
    // Check if user is authenticated
    if (context.User.Identity?.IsAuthenticated != true)
    {
    return; // Not authenticated, don't succeed
    }
    // Get user's roles from JWT claims
    var roles = await userService.GetUserRolesAsync(context.User);
    // Get all role-permission mappings from database
    var rolePermissionMappings = await rolePermissionService.GetRolePermissionMappingsAsync();
    // Build list of permissions for user's roles
    // Format: "PageName.OperationNamePolicy" (e.g., "Contacts.CreatePolicy")
    var userPermissions = rolePermissionMappings
    .Where(rpm => roles.Contains(rpm.RoleName))
    .Select(rpm => $"{rpm.PageName}.{rpm.OperationName}Policy");
    // Check if user has the required permission
    if (userPermissions.Contains(requirement.Permission))
    {
    context.Succeed(requirement);
    }
    }
    }

    2.6 Registering Authorization Services

    In Program.cs, register the authorization components:

    Contact.Api/Program.cs
    // Add the custom policy provider
    builder.Services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
    // Add the permission handler
    builder.Services.AddScoped<IAuthorizationHandler, PermissionHandler>();
    // Configure JWT authentication
    builder.Services.AddAuthentication(options =>
    {
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
    options.TokenValidationParameters = new TokenValidationParameters
    {
    ValidateIssuer = true,
    ValidateAudience = true,
    ValidateLifetime = true,
    ValidateIssuerSigningKey = true,
    ValidIssuer = builder.Configuration["Jwt:Issuer"],
    ValidAudience = builder.Configuration["Jwt:Audience"],
    IssuerSigningKey = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!))
    };
    });

    3. Authentication Flow and Permission Delivery

    3.1 How Users Get Their Permissions

    When a user logs in, the backend returns both a JWT token and the complete list of role permissions. That lets the frontend make permission decisions without additional API calls.

    sequenceDiagram
    participant User
    participant Angular as Angular App
    participant API as Backend API
    participant DB as Database
    User->>Angular: Enter credentials
    Angular->>API: POST /api/users/authenticate
    API->>DB: Validate credentials
    DB-->>API: User found
    API->>DB: Get user roles (UserRoles table)
    DB-->>API: User has "Admin" role
    API->>DB: Get role permissions (RolePermissions + Permissions)
    DB-->>API: Return all permissions for "Admin"
    API->>API: Generate JWT with role claims
    API-->>Angular: { token, user, rolePermissions[] }
    Angular->>Angular: Store in localStorage
    Angular->>Angular: Update AuthStateService
    User->>Angular: Navigate to protected route
    Angular->>Angular: PermissionGuard checks permission
    Angular->>Angular: PermissionService.hasPermission()
    alt Has Permission
    Angular-->>User: Show page
    else No Permission
    Angular-->>User: Redirect with error message
    end

    3.2 Backend: UserService.Authenticate()

    The authentication endpoint returns the user’s role permissions:

    Contact.Application/Services/UserService.cs
    public async Task<AuthenticateResponse?> Authenticate(AuthenticateRequest model)
    {
    // Find user by email
    var user = await _userRepository.GetUserByEmail(model.Email);
    if (user == null) return null;
    // Verify password
    if (!BCrypt.Net.BCrypt.Verify(model.Password, user.Password))
    return null;
    // Generate JWT token with role claims
    var token = GenerateJwtToken(user);
    // Get all role permissions for this user
    var rolePermissions = await GetRolePermissionMappingsAsync(user.Id);
    // Return response with token AND permissions
    return new AuthenticateResponse(user, token, rolePermissions.ToList());
    }

    3.3 Backend: JWT Token Generation

    The JWT token includes the user’s roles as claims, which are used for backend authorization:

    Contact.Application/Services/UserService.cs
    private string GenerateJwtToken(User user)
    {
    var tokenHandler = new JwtSecurityTokenHandler();
    var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
    // Create claims including user roles
    var claims = new List<Claim>
    {
    new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
    new Claim(ClaimTypes.Email, user.Email),
    new Claim(ClaimTypes.Name, user.Username)
    };
    // Add role claims (used by PermissionHandler)
    var roles = _userRoleRepository.GetUserRolesAsync(user.Id).Result;
    foreach (var role in roles)
    {
    claims.Add(new Claim(ClaimTypes.Role, role.Name));
    }
    var tokenDescriptor = new SecurityTokenDescriptor
    {
    Subject = new ClaimsIdentity(claims),
    Expires = DateTime.UtcNow.AddDays(7),
    SigningCredentials = new SigningCredentials(
    new SymmetricSecurityKey(key),
    SecurityAlgorithms.HmacSha256Signature),
    Issuer = _appSettings.Issuer,
    Audience = _appSettings.Audience
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
    }

    4. Frontend Permission System (Angular)

    The Angular frontend implements a complete permission system with three main components:

    1. PermissionService - Checks if user has specific permissions
    2. PermissionGuard - Protects routes
    3. HasPermission Directive - Conditionally shows/hides UI elements

    4.1 Auth State Service

    First, let’s see how the user state (including permissions) is managed:

    // Location: frontend/src/app/@core/services/auth-state.service.ts
    import { Injectable, signal, computed } from '@angular/core';
    import { User } from '@core/models/user.model';
    @Injectable({ providedIn: 'root' })
    export class AuthStateService {
    // Signal-based state management
    private userState = signal<User | null>(null);
    // Computed value for easy access
    readonly user = computed(() => this.userState());
    readonly isLoggedIn = computed(() => !!this.userState());
    constructor() {
    // Initialize from localStorage on app start
    this.initializeFromStorage();
    }
    private initializeFromStorage(): void {
    if (typeof window !== 'undefined') {
    const stored = localStorage.getItem('currentUser');
    if (stored) {
    try {
    const user = JSON.parse(stored);
    this.userState.set(user);
    } catch {
    localStorage.removeItem('currentUser');
    }
    }
    }
    }
    updateUser(user: User | null): void {
    this.userState.set(user);
    if (user) {
    localStorage.setItem('currentUser', JSON.stringify(user));
    } else {
    localStorage.removeItem('currentUser');
    }
    }
    getUser(): User | null {
    return this.userState();
    }
    }

    4.2 Permission Service

    This service provides the core permission checking logic:

    // Location: frontend/src/app/@core/services/permission.service.ts
    import { Injectable, inject } from '@angular/core';
    import { AuthStateService } from './auth-state.service';
    @Injectable({ providedIn: 'root' })
    export class PermissionService {
    private authStateService = inject(AuthStateService);
    /**
    * Check if current user has permission for a specific page and operation
    * @param page - The page/resource name (e.g., 'Contacts', 'Users')
    * @param operation - The operation name (e.g., 'Read', 'Create', 'Update', 'Delete')
    * @returns true if user has the permission, false otherwise
    */
    hasPermission(page: string, operation: string): boolean {
    const user = this.authStateService.getUser();
    // No user = no permissions
    if (!user || !user.rolePermissions) {
    return false;
    }
    // Check if any of user's role permissions match
    return user.rolePermissions.some(
    (permission) =>
    permission.pageName === page &&
    permission.operationName === operation
    );
    }
    }

    4.3 Permission Guard

    Protects routes from unauthorized access:

    // Location: frontend/src/app/@core/guards/permission.guard.ts
    import { inject } from '@angular/core';
    import { CanActivateFn, Router } from '@angular/router';
    import { PermissionService } from '@core/services/permission.service';
    import { MatSnackBar } from '@angular/material/snack-bar';
    /**
    * Factory function that creates a permission guard for specific page/operation
    * @param page - The page name to check permission for
    * @param operation - The operation to check permission for
    * @returns CanActivateFn that checks the permission
    */
    export function permissionGuard(page: string, operation: string): CanActivateFn {
    return () => {
    const permissionService = inject(PermissionService);
    const router = inject(Router);
    const snackBar = inject(MatSnackBar);
    // Check if user has the required permission
    if (permissionService.hasPermission(page, operation)) {
    return true;
    }
    // Show error message and redirect
    snackBar.open(
    `Access Denied: You don't have permission to ${operation.toLowerCase()} ${page.toLowerCase()}`,
    'Close',
    { duration: 5000, panelClass: ['error-snackbar'] }
    );
    // Redirect to home or dashboard
    router.navigate(['/']);
    return false;
    };
    }

    Usage in Routes:

    frontend/src/app/feature/contact-person/contact-person.routes.ts
    import { Routes } from '@angular/router';
    import { permissionGuard } from '@core/guards/permission.guard';
    export const CONTACT_ROUTES: Routes = [
    {
    path: '',
    component: ContactListComponent,
    canActivate: [permissionGuard('Contacts', 'Read')]
    },
    {
    path: 'add',
    component: ContactFormComponent,
    canActivate: [permissionGuard('Contacts', 'Create')]
    },
    {
    path: 'edit/:id',
    component: ContactFormComponent,
    canActivate: [permissionGuard('Contacts', 'Update')]
    }
    ];

    4.4 HasPermission Directive

    This structural directive conditionally renders UI elements based on permissions:

    // Location: frontend/src/app/@shared/directives/permission.directive.ts
    import {
    Directive,
    Input,
    TemplateRef,
    ViewContainerRef,
    inject,
    OnInit,
    } from '@angular/core';
    import { PermissionService } from '@core/services/permission.service';
    @Directive({
    selector: '[hasPermission]',
    standalone: true,
    })
    export class HasPermissionDirective implements OnInit {
    private templateRef = inject(TemplateRef<any>);
    private viewContainer = inject(ViewContainerRef);
    private permissionService = inject(PermissionService);
    @Input('hasPermission') permission!: { page: string; operation: string };
    ngOnInit(): void {
    this.updateView();
    }
    private updateView(): void {
    // Clear any existing view
    this.viewContainer.clear();
    // Check permission and render if allowed
    if (this.permissionService.hasPermission(
    this.permission.page,
    this.permission.operation
    )) {
    // Create the view (show the element)
    this.viewContainer.createEmbeddedView(this.templateRef);
    }
    // If no permission, element is not rendered at all
    }
    }

    Usage in Templates:

    {/* Location: frontend/src/app/feature/contact-person/contact-list/contact-list.component.html */}
    {/* Add button only visible to users with Create permission */}
    <button
    mat-raised-button
    color="primary"
    *hasPermission="{ page: 'Contacts', operation: 'Create' }"
    routerLink="add">
    <mat-icon>add</mat-icon>
    Add Contact
    </button>
    {/* Edit button in table row */}
    <button
    mat-icon-button
    *hasPermission="{ page: 'Contacts', operation: 'Update' }"
    [routerLink]="['edit', contact.id]">
    <mat-icon>edit</mat-icon>
    </button>
    {/* Delete button in table row */}
    <button
    mat-icon-button
    color="warn"
    *hasPermission="{ page: 'Contacts', operation: 'Delete' }"
    (click)="deleteContact(contact)">
    <mat-icon>delete</mat-icon>
    </button>

    4.5 Permission-Based Navigation Menu

    The sidebar navigation filters menu items based on user permissions:

    // Location: frontend/src/app/@shared/components/custom-sidenav/custom-sidenav.component.ts
    import { Component, inject } from '@angular/core';
    import { PermissionService } from '@core/services/permission.service';
    interface MenuItem {
    icon: string;
    label: string;
    route: string;
    page: string; // Permission page
    operation: string; // Permission operation
    }
    @Component({
    selector: 'app-custom-sidenav',
    templateUrl: './custom-sidenav.component.html',
    standalone: true,
    // ... imports
    })
    export class CustomSidenavComponent {
    private permissionService = inject(PermissionService);
    // All possible menu items
    allMenuItems: MenuItem[] = [
    { icon: 'contacts', label: 'Contacts', route: '/contact', page: 'Contacts', operation: 'Read' },
    { icon: 'people', label: 'Users', route: '/user', page: 'Users', operation: 'Read' },
    { icon: 'admin_panel_settings', label: 'Roles', route: '/admin/role', page: 'Roles', operation: 'Read' },
    { icon: 'lock', label: 'Permissions', route: '/admin/permission', page: 'Permissions', operation: 'Read' },
    { icon: 'assignment_ind', label: 'Role Permissions', route: '/admin/role-permission', page: 'RolePermissions', operation: 'Read' },
    ];
    // Filter menu items based on permissions
    get visibleMenuItems(): MenuItem[] {
    return this.allMenuItems.filter(item =>
    this.permissionService.hasPermission(item.page, item.operation)
    );
    }
    // Helper method for use in template
    hasPermission(page: string, operation: string): boolean {
    return this.permissionService.hasPermission(page, operation);
    }
    }

    5. Admin UI: Role-Permission Mapping

    The application includes an admin interface for managing role permissions. This allows administrators to assign and revoke permissions without code changes.

    5.1 Role Permission Mapping Screen

    1Select role dropdownpick 'Admin'2Expandable page listexpand 'Contacts'3Operation checkboxescheck Read, Create, Update4Save buttonwrites to RolePermissions
    The mapping screen is four decisions in a row, and the last one is the only one that writes.

    5.2 Role Permission Mapping Component

    frontend/src/app/feature/admin/role-permission-mapping/role-permission-mapping.component.ts
    import { Component, OnInit, inject, signal } from '@angular/core';
    import { RolePermissionService } from '@core/services/role-permission.service';
    import { RoleService } from '@core/services/role.service';
    import { PermissionService as AdminPermissionService } from '@core/services/admin-permission.service';
    import { NotificationService } from '@core/services/notification.service';
    interface PageWithOperations {
    pageId: string;
    pageName: string;
    expanded: boolean;
    operations: OperationPermission[];
    }
    interface OperationPermission {
    operationId: string;
    operationName: string;
    permissionId: string;
    isSelected: boolean;
    }
    @Component({
    selector: 'app-role-permission-mapping',
    templateUrl: './role-permission-mapping.component.html',
    standalone: true,
    // ... imports
    })
    export class RolePermissionMappingComponent implements OnInit {
    private roleService = inject(RoleService);
    private permissionService = inject(AdminPermissionService);
    private rolePermissionService = inject(RolePermissionService);
    private notificationService = inject(NotificationService);
    // State using signals
    roles = signal<Role[]>([]);
    selectedRoleId = signal<string>('');
    pages = signal<PageWithOperations[]>([]);
    loading = signal<boolean>(false);
    ngOnInit(): void {
    this.loadRoles();
    }
    async loadRoles(): Promise<void> {
    this.roleService.getRoles().subscribe({
    next: (roles) => this.roles.set(roles),
    error: (err) => this.notificationService.error('Failed to load roles')
    });
    }
    onRoleChange(roleId: string): void {
    this.selectedRoleId.set(roleId);
    this.loadPermissionsForRole(roleId);
    }
    async loadPermissionsForRole(roleId: string): Promise<void> {
    this.loading.set(true);
    // Get all permissions grouped by page
    this.permissionService.getPermissionsGroupedByPage().subscribe({
    next: (groupedPermissions) => {
    // Get current role's permissions
    this.rolePermissionService.getRolePermissions(roleId).subscribe({
    next: (rolePermissions) => {
    // Mark which permissions are selected for this role
    const pages = groupedPermissions.map(page => ({
    ...page,
    expanded: false,
    operations: page.operations.map(op => ({
    ...op,
    isSelected: rolePermissions.some(rp => rp.permissionId === op.permissionId)
    }))
    }));
    this.pages.set(pages);
    this.loading.set(false);
    }
    });
    }
    });
    }
    togglePage(pageIndex: number): void {
    const pages = [...this.pages()];
    pages[pageIndex].expanded = !pages[pageIndex].expanded;
    this.pages.set(pages);
    }
    togglePermission(pageIndex: number, opIndex: number): void {
    const pages = [...this.pages()];
    pages[pageIndex].operations[opIndex].isSelected =
    !pages[pageIndex].operations[opIndex].isSelected;
    this.pages.set(pages);
    }
    savePermissions(): void {
    const roleId = this.selectedRoleId();
    // Collect all selected permission IDs
    const selectedPermissionIds = this.pages()
    .flatMap(page => page.operations)
    .filter(op => op.isSelected)
    .map(op => op.permissionId);
    const payload = {
    roleId: roleId,
    permissionIds: selectedPermissionIds
    };
    this.rolePermissionService.saveRolePermissions(payload).subscribe({
    next: () => {
    this.notificationService.success('Permissions saved successfully');
    },
    error: (err) => {
    this.notificationService.error('Failed to save permissions');
    }
    });
    }
    }

    5.3 Role Permission Mapping Template

    {/* Location: frontend/src/app/feature/admin/role-permission-mapping/role-permission-mapping.component.html */}
    <div class="role-permission-container">
    <mat-card>
    <mat-card-header>
    <mat-card-title>Role Permission Mapping</mat-card-title>
    <mat-card-subtitle>Assign permissions to roles</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
    {/* Role Selection */}
    <mat-form-field appearance="outline" class="full-width">
    <mat-label>Select Role</mat-label>
    <mat-select (selectionChange)="onRoleChange($event.value)">
    @for (role of roles(); track role.id) {
    <mat-option [value]="role.id">{{ role.name }}</mat-option>
    }
    </mat-select>
    </mat-form-field>
    {/* Permission Matrix */}
    @if (selectedRoleId()) {
    <div class="permission-matrix">
    @for (page of pages(); track page.pageId; let i = $index) {
    <mat-expansion-panel [expanded]="page.expanded" (opened)="togglePage(i)">
    <mat-expansion-panel-header>
    <mat-panel-title>
    <mat-icon>folder</mat-icon>
    {{ page.pageName }}
    </mat-panel-title>
    <mat-panel-description>
    {{ page.operations.filter(o => o.isSelected).length }} / {{ page.operations.length }} permissions
    </mat-panel-description>
    </mat-expansion-panel-header>
    <div class="operations-grid">
    @for (operation of page.operations; track operation.operationId; let j = $index) {
    <mat-checkbox
    [checked]="operation.isSelected"
    (change)="togglePermission(i, j)">
    {{ operation.operationName }}
    </mat-checkbox>
    }
    </div>
    </mat-expansion-panel>
    }
    </div>
    {/* Save Button */}
    <div class="actions">
    <button
    mat-raised-button
    color="primary"
    (click)="savePermissions()"
    [disabled]="loading()">
    <mat-icon>save</mat-icon>
    Save Permissions
    </button>
    </div>
    }
    </mat-card-content>
    </mat-card>
    </div>

    6. Complete Permission Flow Summary

    Here’s how everything works together when a user interacts with the application:

    Phase 1: User Login

    1User enters credentials2API validates3Generate JWT4Fetch role permissions5Return token + permissions6Store in localStorage
    Login ends with the permission set already in the browser, which is what makes every later check a local lookup rather than a round trip.

    Phase 2: Route Navigation

    1User clicks Contacts2PermissionGuardHas Contacts.Read?yesLoad componentnoRedirect + error
    Route navigation. The guard runs before the component is loaded, so a user without the permission never downloads the screen.

    Phase 3: UI Rendering

    1Component loads2*hasPermission directiveHas Contacts.Create?yesShow Add buttonnoHide Add button
    UI rendering. The directive removes the element rather than disabling it, so an unpermitted action is not in the DOM to be re-enabled.

    Phase 4: API Authorization

    1Submit form2POST /api/contacts3AuthorizePermission4PolicyProvider5PermissionHandlerRole has permission?yes201 Createdno403 Forbidden
    API authorization. The four server-side steps repeat the check the browser already made, because the browser's answer is a convenience and this one is the control.

    Complete Flow Overview

    PhaseComponentActionResult
    1. LoginAuthServiceAuthenticate userJWT + permissions stored
    2. NavigatePermissionGuardCheck route accessAllow or redirect
    3. Render*hasPermissionCheck UI visibilityShow or hide elements
    4. API CallPermissionHandlerValidate requestProcess or reject

    Gotchas

    A few rough edges in this implementation worth sanding down before production:

    • GenerateJwtToken blocks on async. _userRoleRepository.GetUserRolesAsync(user.Id).Result does sync-over-async inside the token builder. Under load that can starve the thread pool or deadlock. Make the method async and await the roles instead.
    • Permissions in localStorage go stale. The frontend caches role permissions at login. If an admin changes a role while the user is signed in, their UI keeps the old permissions until they log out and back in. The API still enforces the current rules, so it isn’t a security hole, but the UI lies. Re-fetch permissions on navigation (or push changes over SignalR) if that gap matters.
    • Seven-day tokens with no revocation. The JWT is issued with a 7-day expiry and no refresh or revocation path. Fine for a sample; shorten it and add refresh tokens for anything real.

    7. Why This Design Holds Up

    • Dynamic and flexible: permissions change without an app restart; new pages and operations are added in the database and mapped through the admin UI, no code change.
    • Granular: every API endpoint and every UI element can carry its own permission, and users can hold multiple roles.
    • Clean Architecture compliant: authorization logic stays out of the domain and business layers; infrastructure handles the permission queries.
    • Enforced on both sides: guards and directives gate the Angular UI, custom attributes gate the API, so bypassing the UI still hits a server-side check.
    • Maintainable and testable: permission checks live in dedicated services with a clear seam for unit tests.

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.