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:
- Entities and relationships for RBAC.
- Dynamic policy registration during application startup.
- Custom authorization attributes for securing API endpoints.
- Custom permission handler to validate user access.
- 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:
- Clean Architecture: Introduction to the Project Structure
- Clean Architecture: Implementing AutoMapper for DTO Mapping and Audit Logging
- Clean Architecture: Validating Inputs with FluentValidation
- Clean Architecture: Dependency Injection Setup Across Layers
- Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) (You are here)
- Clean Architecture: Implementing Activity Logging with Custom Attributes
- Clean Architecture: Unit of Work Pattern and Its Role in Managing Transactions
- Clean Architecture: Using Dapper for Data Access and Repository Pattern
- Clean Architecture: Best Practices for Creating and Using DTOs in the API
- Clean Architecture: Error Handling and Exception Management in the API
- Clean Architecture: Dockerizing the .NET Core API and MS SQL Server
- 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; }
}
Id | Name | Url / Feature / Api |
---|---|---|
3f2b1c4d-1234-5678-8901-abcd12345678 | Contacts | /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; }
}
Id | Name | Description |
---|---|---|
op-1-1234-5678-8901-abcd12345678 | Read | Allows reading data |
op-2-2234-5678-8901-abcd23456789 | Write | Allows writing data |
op-3-3234-5678-8901-abcd34567890 | Update | Allows updating data |
op-4-4234-5678-8901-abcd45678901 | Delete | Allows 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; }
}
Id | PageId | OperationId |
---|---|---|
pom-1-1234-5678-8901-abcd12345678 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-1-1234-5678-8901-abcd12345678 |
pom-2-2234-5678-8901-abcd23456789 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-2-2234-5678-8901-abcd23456789 |
pom-3-3234-5678-8901-abcd34567890 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-3-3234-5678-8901-abcd34567890 |
pom-4-4234-5678-8901-abcd45678901 | 3f2b1c4d-1234-5678-8901-abcd12345678 | op-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; }
}
Id | Name | Description |
---|---|---|
role-1-1234-5678-8901-abcd12345678 | Admin | Full access to Contacts |
role-2-2234-5678-8901-abcd23456789 | User | Read-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; }
}
Id | RoleId | PageOperationMappingId |
---|---|---|
rp-1-1234-5678-8901-abcd12345678 | role-1-1234-5678-8901-abcd12345678 | pom-1-1234-5678-8901-abcd12345678 |
rp-2-2234-5678-8901-abcd23456789 | role-1-1234-5678-8901-abcd12345678 | pom-2-2234-5678-8901-abcd23456789 |
rp-3-3234-5678-8901-abcd34567890 | role-1-1234-5678-8901-abcd12345678 | pom-3-3234-5678-8901-abcd34567890 |
rp-4-4234-5678-8901-abcd45678901 | role-1-1234-5678-8901-abcd12345678 | pom-4-4234-5678-8901-abcd45678901 |
rp-5-1234-5678-8901-abcd56789012 | role-2-2234-5678-8901-abcd23456789 | pom-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; }
}
Id | UserId | RoleId |
---|---|---|
ur-1-1234-5678-8901-abcd12345678 | user-1-1234-5678-8901-abcd12345678 | role-1-1234-5678-8901-abcd12345678 |
ur-2-2234-5678-8901-abcd23456789 | user-2-2234-5678-8901-abcd23456789 | role-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; }
}
Id | FirstName | LastName | Username | Mobile | Password | |
---|---|---|---|---|---|---|
user-1-1234-5678-8901-abcd12345678 | Alice | Smith | alice.smith | 1234567890 | alice@email.com | hashedPw1 |
user-2-2234-5678-8901-abcd23456789 | Bob | Johnson | bob.johnson | 9876543210 | bob@email.com | hashedPw2 |
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
- Granular Access Control: Each API action can be tied to a fine-grained permission.
- Dynamic Scalability: New permissions can be added without rewriting existing code.
- Modularity: Each layer (API, Application, Infrastructure) handles its responsibilities.
- 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
[…] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]
[…] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]
[…] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]
[…] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]
[…] Clean Architecture: Handling Authorization and Role-Based Access Control (RBAC) […]