LowCodeHub.Permissions
0.0.10
dotnet add package LowCodeHub.Permissions --version 0.0.10
NuGet\Install-Package LowCodeHub.Permissions -Version 0.0.10
<PackageReference Include="LowCodeHub.Permissions" Version="0.0.10" />
<PackageVersion Include="LowCodeHub.Permissions" Version="0.0.10" />
<PackageReference Include="LowCodeHub.Permissions" />
paket add LowCodeHub.Permissions --version 0.0.10
#r "nuget: LowCodeHub.Permissions, 0.0.10"
#:package LowCodeHub.Permissions@0.0.10
#addin nuget:?package=LowCodeHub.Permissions&version=0.0.10
#tool nuget:?package=LowCodeHub.Permissions&version=0.0.10
LowCodeHub.Permissions
A permission-based authorization library for ASP.NET Core Minimal APIs. Dynamic policy provider, endpoint filter, and inline permission checks — all powered by your own IPermissionService implementation.
Why This Library?
| Feature | LowCodeHub.Permissions | ASP.NET Core Policies | Custom Authorization |
|---|---|---|---|
| Policy creation | Dynamic — policies created on demand | Static — register each policy at startup | Manual |
| Permission source | Your implementation — DB, cache, token, anything | Claims/roles only | Manual |
| Endpoint protection | .RequirePermission("perm") one-liner |
[Authorize(Policy = "...")] |
Manual filter |
| Inline checks | CheckPermissionAsync() helper |
IAuthorizationService.AuthorizeAsync() |
Manual |
| Multi-permission OR | CheckAnyPermissionAsync() built-in |
Manual policy with multiple requirements | Manual |
Installation
dotnet add package LowCodeHub.Permissions
Quick Start
builder.Services.AddPermissionAuthorization<MyPermissionService>();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/orders", GetOrders)
.RequirePermission("orders.read");
app.MapPost("/orders", CreateOrder)
.RequirePermission("orders.create");
That's it. Each endpoint is protected by a named permission. The library dynamically creates authorization policies for each permission name and delegates the check to your IPermissionService implementation — no need to pre-register policies at startup.
Table of Contents
- Implement IPermissionService
- Service Registration
- Protecting Endpoints
- Inline Permission Checks
- How It Works
- Requirements
- License
Implement IPermissionService
Provide your own permission logic by implementing IPermissionService:
public interface IPermissionService
{
bool IsAuthenticated { get; }
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken = default);
}
Example implementation (reading from JWT claims):
public sealed class ClaimsPermissionService(IHttpContextAccessor httpContextAccessor) : IPermissionService
{
public bool IsAuthenticated
=> httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
public Task<bool> HasPermissionAsync(string permission, CancellationToken ct)
{
var hasPermission = httpContextAccessor.HttpContext?.User
.Claims.Any(c => c.Type == "permission" && c.Value == permission) ?? false;
return Task.FromResult(hasPermission);
}
}
Example implementation (reading from a database):
public sealed class DbPermissionService(
IHttpContextAccessor httpContextAccessor,
IPermissionRepository repository) : IPermissionService
{
public bool IsAuthenticated
=> httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false;
public async Task<bool> HasPermissionAsync(string permission, CancellationToken ct)
{
var userId = httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null) return false;
return await repository.HasPermissionAsync(userId, permission, ct);
}
}
Service Registration
With your implementation
builder.Services.AddPermissionAuthorization<MyPermissionService>();
This registers MyPermissionService as IPermissionService and adds the authorization handler + dynamic policy provider.
Both overloads also register IHttpContextAccessor — the library itself does not use it, but most IPermissionService implementations need it to read the current user (as in the examples above). All registrations use TryAdd semantics, so calling AddPermissionAuthorization more than once is safe and an IPermissionService you registered yourself is never replaced.
Separate registration
If you register IPermissionService yourself:
builder.Services.AddScoped<IPermissionService, MyPermissionService>();
builder.Services.AddPermissionAuthorization();
Protecting Endpoints
Endpoint filter (recommended)
app.MapGet("/orders", GetOrders)
.RequirePermission("orders.read");
app.MapPost("/orders", CreateOrder)
.RequirePermission("orders.create");
app.MapDelete("/orders/{id}", DeleteOrder)
.RequirePermission("orders.delete");
The endpoint filter checks the permission before the handler executes:
- Not authenticated →
401 Unauthorized - Authenticated but no permission →
403 Forbidden - Has permission → handler executes normally
Inline Permission Checks
For fine-grained control within a handler, use the extension methods:
Single permission
app.MapPost("/orders/{id}/cancel", async (
int id, IPermissionService permissions, CancellationToken ct) =>
{
var denied = await permissions.CheckPermissionAsync("orders.cancel", ct);
if (denied is not null) return denied; // 401 or 403
// Permission granted — proceed
return Results.Ok();
});
Any of multiple permissions (OR logic)
app.MapGet("/reports", async (IPermissionService permissions, CancellationToken ct) =>
{
var denied = await permissions.CheckAnyPermissionAsync(
["reports.read", "admin.reports"], ct);
if (denied is not null) return denied;
return Results.Ok();
});
All of multiple permissions (AND logic)
app.MapPost("/orders/{id}/refund", async (
int id, IPermissionService permissions, CancellationToken ct) =>
{
var denied = await permissions.CheckAllPermissionsAsync(
["orders.update", "payments.refund"], ct);
if (denied is not null) return denied;
return Results.Ok();
});
All three helpers return null if authorized, or a 401/403 ProblemDetails result if not — the same response shape the .RequirePermission() endpoint filter produces. CheckAnyPermissionAsync and CheckAllPermissionsAsync throw ArgumentException if the permission list is empty or contains a null/whitespace entry, so a misconfigured (empty) permission list can never silently authorize a request.
Note: The
[Authorize(Policy = "...")]path is enforced by ASP.NET Core's authorization middleware, which returns its own (empty-body)401/403responses rather than ProblemDetails.
How It Works
┌─────────────────────────────────────────────────────────┐
│ .RequirePermission("orders.read") │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PermissionEndpointFilter │
│ 1. Resolve IPermissionService from DI │
│ 2. Check IsAuthenticated → 401 if not │
│ 3. Call HasPermissionAsync("orders.read") │
│ ├── true → invoke next (handler executes) │
│ └── false → return 403 Forbidden │
└─────────────────────────────────────────────────────────┘
─── OR (for [Authorize(Policy = "...")] attributes) ───
┌─────────────────────────────────────────────────────────┐
│ PermissionAuthorizationPolicyProvider │
│ Creates authorization policies dynamically: │
│ Policy "orders.read" → PermissionRequirement("orders.read") │
└─────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PermissionAuthorizationHandler │
│ Evaluates PermissionRequirement against │
│ IPermissionService.HasPermissionAsync() │
└─────────────────────────────────────────────────────────┘
- Dynamic policy provider — When ASP.NET Core encounters an
[Authorize(Policy = "orders.read")]attribute, the provider dynamically creates a policy with aPermissionRequirement. - Authorization handler — Evaluates the requirement by calling your
IPermissionService.HasPermissionAsync(). - Endpoint filter — For Minimal APIs, the
.RequirePermission()extension adds a filter that checks permissions before the handler runs. It also callsRequireAuthorization()so the endpoint carries authorization metadata (OpenAPI security requirements,[AllowAnonymous]interplay, fallback-policy auditing).
Warning — every unknown policy name becomes a permission. The dynamic policy provider treats any policy name it doesn't find among your registered policies as a permission and delegates it to
IPermissionService.HasPermissionAsync. A typo like[Authorize(Policy = "Admni")]will not fail at startup — it becomes a permission check that (presumably) always denies with403. The behavior fails closed, but typos surface as runtime 403s instead of configuration errors, so keep permission names in shared constants.
Requirements
- .NET 10 or later
License
MIT © Ahmed Abuelnour
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.