LowCodeHub.Permissions 0.0.10

dotnet add package LowCodeHub.Permissions --version 0.0.10
                    
NuGet\Install-Package LowCodeHub.Permissions -Version 0.0.10
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LowCodeHub.Permissions" Version="0.0.10" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LowCodeHub.Permissions" Version="0.0.10" />
                    
Directory.Packages.props
<PackageReference Include="LowCodeHub.Permissions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LowCodeHub.Permissions --version 0.0.10
                    
#r "nuget: LowCodeHub.Permissions, 0.0.10"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LowCodeHub.Permissions@0.0.10
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LowCodeHub.Permissions&version=0.0.10
                    
Install as a Cake Addin
#tool nuget:?package=LowCodeHub.Permissions&version=0.0.10
                    
Install as a Cake Tool

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.

NuGet License: MIT

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

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

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/403 responses 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()                │
└─────────────────────────────────────────────────────────┘
  1. Dynamic policy provider — When ASP.NET Core encounters an [Authorize(Policy = "orders.read")] attribute, the provider dynamically creates a policy with a PermissionRequirement.
  2. Authorization handler — Evaluates the requirement by calling your IPermissionService.HasPermissionAsync().
  3. Endpoint filter — For Minimal APIs, the .RequirePermission() extension adds a filter that checks permissions before the handler runs. It also calls RequireAuthorization() 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 with 403. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
0.0.10 574 6/21/2026
0.0.2 122 5/18/2026
0.0.1 188 3/26/2026