Handling Authorization and Role-Based Access Control (RBAC)

Handling Authorization and Role-Based Access Control (RBAC)
Handling Authorization and Role-Based Access Control (RBAC)

Introduction

Role-Based Access Control (RBAC) ensures that users only perform actions they are authorized to. In the Contact Management Application, a robust RBAC system enforces permissions like Contacts.Create, Contacts.Update, and Contacts.Delete using dynamic policies and custom attributes. This approach supports granular permissions, making the system scalable and secure.

This article provides a detailed, step-by-step guide to implementing RBAC in the Contact Management Application, covering:

  1. Entities and relationships for RBAC.
  2. Dynamic policy registration during application startup.
  3. Custom authorization attributes for securing API endpoints.
  4. Custom permission handler to validate user access.
  5. The benefits of this modular and flexible approach.

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 and MS SQL Server
  12. Clean Architecture: Seeding Initial Data Using Docker Compose and SQL Scripts

1. RBAC Architecture and Entities

1.1 Core Entities

The system uses a modular approach where entities define relationships between roles, permissions, pages, and operations.

Page Entity

Represents a resource or section in the application (e.g., Contacts). For example, the “Contacts” feature allows users to interact with contact-related data.

public class Page : BaseEntity
{
    public required string Name { get; set; }
    public required string Url { get; set; }
}
IdNameUrl / Feature / Api
3f2b1c4d-1234-5678-8901-abcd12345678Contacts/contacts


Operation Entity

The Operation entity defines the actions that can be performed on a page / feature/ api. These include operations like reading, writing, updating, and deleting data.

public class Operation : BaseEntity
{
    public required string Name { get; set; }
    public required string Description { get; set; }

}
IdNameDescription
op-1-1234-5678-8901-abcd12345678ReadAllows reading data
op-2-2234-5678-8901-abcd23456789WriteAllows writing data
op-3-3234-5678-8901-abcd34567890UpdateAllows updating data
op-4-4234-5678-8901-abcd45678901DeleteAllows deleting data


PageOperationMapping Entity

The PageOperationMapping entity connects a page to its corresponding operations. This forms the base for assigning granular permissions.

public class PageOperationMapping : BaseEntity
{
    public string PageName { get; set; }
    public string OperationName { get; set; }
}
IdPageIdOperationId
pom-1-1234-5678-8901-abcd123456783f2b1c4d-1234-5678-8901-abcd12345678op-1-1234-5678-8901-abcd12345678
pom-2-2234-5678-8901-abcd234567893f2b1c4d-1234-5678-8901-abcd12345678op-2-2234-5678-8901-abcd23456789
pom-3-3234-5678-8901-abcd345678903f2b1c4d-1234-5678-8901-abcd12345678op-3-3234-5678-8901-abcd34567890
pom-4-4234-5678-8901-abcd456789013f2b1c4d-1234-5678-8901-abcd12345678op-4-4234-5678-8901-abcd45678901


Role Entity

The Role entity defines user roles such as Admin or User. Roles determine the level of access users have to different operations on a page.

public class Role : BaseEntity
{
    public required string Name { get; set; }
    public required string Description { get; set; }
}
IdNameDescription
role-1-1234-5678-8901-abcd12345678AdminFull access to Contacts
role-2-2234-5678-8901-abcd23456789UserRead-only access to Contacts


RolePermission Entity

The RolePermission entity connects roles to specific permissions by linking to the PageOperationMapping table. This determines what operations each role can perform on a page.

public class RolePermission : BaseEntity
{
    public Guid RoleId { get; set; }
    public Guid PermissionId { get; set; }
}
IdRoleIdPageOperationMappingId
rp-1-1234-5678-8901-abcd12345678role-1-1234-5678-8901-abcd12345678pom-1-1234-5678-8901-abcd12345678
rp-2-2234-5678-8901-abcd23456789role-1-1234-5678-8901-abcd12345678pom-2-2234-5678-8901-abcd23456789
rp-3-3234-5678-8901-abcd34567890role-1-1234-5678-8901-abcd12345678pom-3-3234-5678-8901-abcd34567890
rp-4-4234-5678-8901-abcd45678901role-1-1234-5678-8901-abcd12345678pom-4-4234-5678-8901-abcd45678901
rp-5-1234-5678-8901-abcd56789012role-2-2234-5678-8901-abcd23456789pom-1-1234-5678-8901-abcd12345678


UserRole Entity

The UserRole entity maps users to their roles, defining what permissions they inherit.

public class UserRole : BaseEntity
{
    public Guid UserId { get; set; }
    public Guid RoleId { get; set; }
}

IdUserIdRoleId
ur-1-1234-5678-8901-abcd12345678user-1-1234-5678-8901-abcd12345678role-1-1234-5678-8901-abcd12345678
ur-2-2234-5678-8901-abcd23456789user-2-2234-5678-8901-abcd23456789role-2-2234-5678-8901-abcd23456789


User Entity

The User entity represents application users. Each user can be assigned a role that determines their access level.

public class User : BaseEntity
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public required string Username { get; set; }
    public required long Mobile { get; set; }
    public required string Email { get; set; }
    public required string Password { get; set; }
}
IdFirstNameLastNameUsernameMobileEmailPassword
user-1-1234-5678-8901-abcd12345678AliceSmithalice.smith1234567890alice@email.comhashedPw1
user-2-2234-5678-8901-abcd23456789BobJohnsonbob.johnson9876543210bob@email.comhashedPw2

2. Dynamic Policy Registration

Policies are dynamically created at application startup based on the mappings between pages and operations.

2.1 Fetching Permission Mappings

The PermissionRepository retrieves mappings of pages and operations:

public async Task<IEnumerable<PageOperationMapping>> GetPageOperationMappingsAsync()
{
    var sql = @"

        SELECT
            p.Name AS PageName,
            o.Name AS OperationName
        FROM Permissions perm
        INNER JOIN Pages p ON perm.PageId = p.Id
        INNER JOIN Operations o ON perm.OperationId = o.Id
        ORDER BY p.Name, o.Name;";

    return await _dapperHelper.GetAll<PageOperationMapping>(sql, null);
}

2.2 Registering Policies in Startup.cs

Dynamic policies are added based on the mappings fetched by IPermissionService:

builder.Services.AddAuthorization(options =>

{

    var permissionService = builder.Services.BuildServiceProvider().GetRequiredService<IPermissionService>();

    var permissionMappings = permissionService.GetAllPageOperationMappingsAsync().Result;

    foreach (var mapping in permissionMappings)

    {

        var policyName = $"{mapping.PageName}.{mapping.OperationName}Policy";

        options.AddPolicy(policyName, policy =>

        {

            policy.Requirements.Add(new PermissionRequirement(policyName));

        });

    }

});

3. Custom Authorization Attribute

3.1 AuthorizePermission Attribute

This custom attribute simplifies the application of permissions:

public class AuthorizePermissionAttribute : AuthorizeAttribute
{
    public AuthorizePermissionAttribute(string permission)

    {
        Policy = $"{permission}Policy";
    }
}

3.2 Securing Endpoints

Apply the AuthorizePermission attribute to secure actions in the API:

[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);
}

Each action enforces a specific permission, such as Contacts.Create.

4. Custom Permission Handler

4.1 PermissionHandler Implementation

The PermissionHandler validates whether the current user has the required permissions:

public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
    private readonly IServiceProvider _serviceProvider;
    public PermissionHandler(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)

    {
        using var scope = _serviceProvider.CreateScope();
        var _userService = scope.ServiceProvider.GetRequiredService<IUserService>();
        var _rolePermissionService = scope.ServiceProvider.GetRequiredService<IRolePermissionService>();
        if (context.User.Identity.IsAuthenticated)
        {
            var userId = _userService.GetUserId(context.User);
            var roles = await _userService.GetUserRolesAsync(context.User);
            var rolePermissionMappings = await_rolePermissionService.GetRolePermissionMappingsAsync();

            var userPermissions = rolePermissionMappings
                .Where(rpm => roles.Contains(rpm.RoleName))
                .Select(rpm => $"{rpm.PageName}.{rpm.OperationName}Policy");

            if (userPermissions.Contains(requirement.Permission))
            {
                context.Succeed(requirement);
            }
        }
    }
}

5. Benefits of the RBAC Implementation

  1. Granular Access Control: Each API action can be tied to a fine-grained permission.
  2. Dynamic Scalability: New permissions can be added without rewriting existing code.
  3. Modularity: Each layer (API, Application, Infrastructure) handles its responsibilities.
  4. Maintainability: Simplified management of roles and permissions through dynamic policies.

Conclusion

This article demonstrated how to implement Role-Based Access Control in the Contact Management Application using Clean Architecture principles. By leveraging dynamic policies, custom attributes, and a modular structure, this approach provides flexibility, security, and scalability.

For a more detailed look at the project, you can access the full source code on GitHub:

GitHub Repository: Contact Management Application

In the next article, we will discuss Activity Logging to track user actions within the system.


Discover more from Nitin Singh

Subscribe to get the latest posts sent to your email.

5 Responses

  1. December 16, 2024

    […] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]

  2. December 16, 2024

    […] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]

  3. December 16, 2024

    […] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]

  4. December 16, 2024

    […] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]

  5. December 29, 2024

    […] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]

Leave a Reply