| name | dotnet-permission-authorization |
| description | Implements permission-based authorization with custom attributes, policy providers, and authorization handlers. Provides granular access control beyond simple role-based authorization. |
| version | 1.0.0 |
| language | C# |
| framework | .NET 8+ |
| dependencies | Microsoft.AspNetCore.Authorization |
Permission-Based Authorization Setup
Overview
This skill implements fine-grained permission-based authorization:
- Custom [HasPermission] attribute - Declarative permission requirements
- Policy provider - Dynamically creates policies from permissions
- Authorization handler - Validates user permissions
- Claims transformation - Converts roles to permissions
Quick Reference
| Component | Purpose |
|---|
Permissions | Static permission constants |
Roles | Static role constants |
HasPermissionAttribute | Custom authorize attribute |
PermissionAuthorizationHandler | Validates permissions |
PermissionAuthorizationPolicyProvider | Creates policies dynamically |
RoleToPermissionClaimsTransformation | Maps roles to permissions |
Authorization Structure
/Infrastructure/Authorization/
├── Permissions.cs
├── Roles.cs
├── HasPermissionAttribute.cs
├── PermissionRequirement.cs
├── PermissionAuthorizationHandler.cs
├── PermissionAuthorizationPolicyProvider.cs
├── RoleToPermissionClaimsTransformation.cs
└── AuthorizationExtensions.cs
Template: Permissions Definition
namespace {name}.infrastructure.authorization;
public static class Permissions
{
public const string OrganizationsRead = "organizations:read";
public const string OrganizationsWrite = "organizations:write";
public const string OrganizationsDelete = "organizations:delete";
public const string OrganizationsManageSettings = "organizations:manage_settings";
public const string UsersRead = "users:read";
public const string UsersWrite = "users:write";
public const string UsersDelete = "users:delete";
public const string UsersManageRoles = "users:manage_roles";
public const string DepartmentsRead = "departments:read";
public const string DepartmentsWrite = "departments:write";
public const string DepartmentsDelete = "departments:delete";
public const string AssessmentsRead = "assessments:read";
public const string AssessmentsWrite = "assessments:write";
public const string AssessmentsSubmit = "assessments:submit";
public const string AssessmentsReview = "assessments:review";
public const string ReportsRead = "reports:read";
public const string ReportsExport = "reports:export";
public const string ReportsViewSensitive = "reports:view_sensitive";
public const string AdminAccess = "admin:access";
public const string AdminManageSystem = "admin:manage_system";
}
Template: Roles Definition
namespace {name}.infrastructure.authorization;
public static class Roles
{
public const string SuperAdmin = "SuperAdmin";
public const string Admin = "Admin";
public const string Consultant = "Consultant";
public const string Manager = "Manager";
public const string Associate = "Associate";
public const string Viewer = "Viewer";
}
Template: Role-Permission Mapping
namespace {name}.infrastructure.authorization;
public static class RolePermissions
{
private static readonly Dictionary<string, HashSet<string>> RolePermissionMap = new()
{
[Roles.SuperAdmin] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.OrganizationsWrite,
Permissions.OrganizationsDelete,
Permissions.OrganizationsManageSettings,
Permissions.UsersRead,
Permissions.UsersWrite,
Permissions.UsersDelete,
Permissions.UsersManageRoles,
Permissions.DepartmentsRead,
Permissions.DepartmentsWrite,
Permissions.DepartmentsDelete,
Permissions.AssessmentsRead,
Permissions.AssessmentsWrite,
Permissions.AssessmentsSubmit,
Permissions.AssessmentsReview,
Permissions.ReportsRead,
Permissions.ReportsExport,
Permissions.ReportsViewSensitive,
Permissions.AdminAccess,
Permissions.AdminManageSystem
},
[Roles.Admin] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.OrganizationsWrite,
Permissions.OrganizationsManageSettings,
Permissions.UsersRead,
Permissions.UsersWrite,
Permissions.UsersManageRoles,
Permissions.DepartmentsRead,
Permissions.DepartmentsWrite,
Permissions.DepartmentsDelete,
Permissions.AssessmentsRead,
Permissions.AssessmentsWrite,
Permissions.AssessmentsReview,
Permissions.ReportsRead,
Permissions.ReportsExport,
Permissions.ReportsViewSensitive,
Permissions.AdminAccess
},
[Roles.Consultant] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.UsersRead,
Permissions.DepartmentsRead,
Permissions.AssessmentsRead,
Permissions.AssessmentsReview,
Permissions.ReportsRead,
Permissions.ReportsExport
},
[Roles.Manager] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.UsersRead,
Permissions.DepartmentsRead,
Permissions.DepartmentsWrite,
Permissions.AssessmentsRead,
Permissions.AssessmentsWrite,
Permissions.AssessmentsSubmit,
Permissions.AssessmentsReview,
Permissions.ReportsRead
},
[Roles.Associate] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.UsersRead,
Permissions.DepartmentsRead,
Permissions.AssessmentsRead,
Permissions.AssessmentsSubmit
},
[Roles.Viewer] = new HashSet<string>
{
Permissions.OrganizationsRead,
Permissions.DepartmentsRead,
Permissions.AssessmentsRead,
Permissions.ReportsRead
}
};
public static IReadOnlySet<string> GetPermissionsForRole(string role)
{
return RolePermissionMap.TryGetValue(role, out var permissions)
? permissions
: new HashSet<string>();
}
public static IReadOnlySet<string> GetPermissionsForRoles(IEnumerable<string> roles)
{
var allPermissions = new HashSet<string>();
foreach (var role in roles)
{
var permissions = GetPermissionsForRole(role);
allPermissions.UnionWith(permissions);
}
return allPermissions;
}
public static bool HasPermission(string role, string permission)
{
return RolePermissionMap.TryGetValue(role, out var permissions)
&& permissions.Contains(permission);
}
}
Template: HasPermission Attribute
using Microsoft.AspNetCore.Authorization;
namespace {name}.infrastructure.authorization;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class HasPermissionAttribute : AuthorizeAttribute
{
public HasPermissionAttribute(string permission)
: base(policy: permission)
{
}
}
Template: Permission Requirement
using Microsoft.AspNetCore.Authorization;
namespace {name}.infrastructure.authorization;
public sealed class PermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; }
public PermissionRequirement(string permission)
{
Permission = permission;
}
}
Template: Permission Authorization Handler
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
namespace {name}.infrastructure.authorization;
internal sealed class PermissionAuthorizationHandler
: AuthorizationHandler<PermissionRequirement>
{
private readonly ILogger<PermissionAuthorizationHandler> _logger;
public PermissionAuthorizationHandler(
ILogger<PermissionAuthorizationHandler> logger)
{
_logger = logger;
}
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionRequirement requirement)
{
var permissions = context.User
.FindAll("permission")
.Select(c => c.Value)
.ToHashSet();
if (permissions.Contains(requirement.Permission))
{
_logger.LogDebug(
"User {UserId} authorized for permission {Permission}",
context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
requirement.Permission);
context.Succeed(requirement);
}
else
{
_logger.LogWarning(
"User {UserId} denied permission {Permission}. User has permissions: {Permissions}",
context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
requirement.Permission,
string.Join(", ", permissions));
}
return Task.CompletedTask;
}
}
Template: Permission Policy Provider
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace {name}.infrastructure.authorization;
internal sealed class PermissionAuthorizationPolicyProvider
: DefaultAuthorizationPolicyProvider
{
private readonly AuthorizationOptions _options;
public PermissionAuthorizationPolicyProvider(
IOptions<AuthorizationOptions> options)
: base(options)
{
_options = options.Value;
}
public override async Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy is not null)
{
return policy;
}
policy = new AuthorizationPolicyBuilder()
.AddRequirements(new PermissionRequirement(policyName))
.Build();
_options.AddPolicy(policyName, policy);
return policy;
}
}
Template: Claims Transformation
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
namespace {name}.infrastructure.authorization;
internal sealed class RoleToPermissionClaimsTransformation
: IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var roles = principal
.FindAll(ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
if (!roles.Any())
{
return Task.FromResult(principal);
}
var permissions = RolePermissions.GetPermissionsForRoles(roles);
var identity = principal.Identity as ClaimsIdentity;
if (identity is null)
{
return Task.FromResult(principal);
}
foreach (var permission in permissions)
{
if (!principal.HasClaim("permission", permission))
{
identity.AddClaim(new Claim("permission", permission));
}
}
return Task.FromResult(principal);
}
}
Template: Authorization Registration
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace {name}.infrastructure.authorization;
public static class AuthorizationExtensions
{
public static IServiceCollection AddPermissionAuthorization(
this IServiceCollection services)
{
services.AddSingleton<IAuthorizationPolicyProvider,
PermissionAuthorizationPolicyProvider>();
services.AddScoped<IAuthorizationHandler,
PermissionAuthorizationHandler>();
services.AddScoped<IClaimsTransformation,
RoleToPermissionClaimsTransformation>();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.FallbackPolicy = null;
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole(Roles.SuperAdmin, Roles.Admin));
options.AddPolicy("ManagerOrAbove", policy =>
policy.RequireRole(Roles.SuperAdmin, Roles.Admin, Roles.Manager));
});
return services;
}
}
Usage in Controllers
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using {name}.infrastructure.authorization;
[Authorize]
[ApiController]
[Route("api/v1/users")]
public class UserController : ControllerBase
{
[HttpGet("{id}")]
[HasPermission(Permissions.UsersRead)]
public async Task<IActionResult> GetById(Guid id) { }
[HttpPost]
[HasPermission(Permissions.UsersWrite)]
public async Task<IActionResult> Create([FromBody] CreateUserRequest request) { }
[HttpDelete("{id}")]
[HasPermission(Permissions.UsersDelete)]
public async Task<IActionResult> Delete(Guid id) { }
[HttpPost("{id}/roles")]
[HasPermission(Permissions.UsersWrite)]
[HasPermission(Permissions.UsersManageRoles)]
public async Task<IActionResult> AssignRole(Guid id, [FromBody] AssignRoleRequest request) { }
[HttpGet("admin/dashboard")]
[Authorize(Roles = Roles.SuperAdmin + "," + Roles.Admin)]
public async Task<IActionResult> GetAdminDashboard() { }
[HttpDelete("organization/{orgId}")]
[Authorize(Policy = "AdminOnly")]
public async Task<IActionResult> DeleteOrganization(Guid orgId) { }
}
Usage in Handlers (Imperative)
using Microsoft.AspNetCore.Authorization;
using {name}.application.abstractions.authentication;
internal sealed class DoSomethingCommandHandler
: ICommandHandler<DoSomethingCommand, Guid>
{
private readonly IUserContext _userContext;
private readonly IAuthorizationService _authorizationService;
public DoSomethingCommandHandler(
IUserContext userContext,
IAuthorizationService authorizationService)
{
_userContext = userContext;
_authorizationService = authorizationService;
}
public async Task<Result<Guid>> Handle(
DoSomethingCommand request,
CancellationToken cancellationToken)
{
var authResult = await _authorizationService.AuthorizeAsync(
_userContext.User,
Permissions.SomethingWrite);
if (!authResult.Succeeded)
{
return Result.Failure<Guid>(CommonErrors.Forbidden);
}
}
}
Resource-Based Authorization
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
internal sealed class DocumentAuthorizationHandler
: AuthorizationHandler<OperationAuthorizationRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OperationAuthorizationRequirement requirement,
Document resource)
{
var userId = context.User.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(userId))
{
return Task.CompletedTask;
}
if (resource.OwnerId.ToString() == userId)
{
context.Succeed(requirement);
return Task.CompletedTask;
}
if (requirement.Name == Operations.Read.Name)
{
if (context.User.HasClaim("permission", Permissions.DocumentsRead))
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
public static class Operations
{
public static readonly OperationAuthorizationRequirement Create = new() { Name = "Create" };
public static readonly OperationAuthorizationRequirement Read = new() { Name = "Read" };
public static readonly OperationAuthorizationRequirement Update = new() { Name = "Update" };
public static readonly OperationAuthorizationRequirement Delete = new() { Name = "Delete" };
}
Critical Rules
- Use constants for permissions - No magic strings
- Permission naming convention -
{resource}:{action}
- Roles aggregate permissions - Don't check roles directly
- Claims transformation - Convert roles to permissions early
- Cache policies - Policy provider caches dynamically created policies
- Multiple attributes = AND - All permissions required
- Log authorization failures - For security auditing
- Don't trust client claims - Always validate on server
- Separate authn from authz - Authentication ≠ Authorization
- Resource-based when needed - For ownership checks
Anti-Patterns to Avoid
if (user.IsInRole("Admin") || user.IsInRole("Manager"))
{
}
[HasPermission(Permissions.UsersWrite)]
public async Task<IActionResult> CreateUser() { }
[Authorize(Policy = "users:write")]
[HasPermission(Permissions.UsersWrite)]
var permissions = request.UserPermissions;
Related Skills
dotnet-jwt-authentication - Authentication setup
api-controller-generator - Protected endpoints
dotnet-clean-architecture - Infrastructure layer