Mesch.Authzn
0.0.1
See the version list below for details.
dotnet add package Mesch.Authzn --version 0.0.1
NuGet\Install-Package Mesch.Authzn -Version 0.0.1
<PackageReference Include="Mesch.Authzn" Version="0.0.1" />
<PackageVersion Include="Mesch.Authzn" Version="0.0.1" />
<PackageReference Include="Mesch.Authzn" />
paket add Mesch.Authzn --version 0.0.1
#r "nuget: Mesch.Authzn, 0.0.1"
#:package Mesch.Authzn@0.0.1
#addin nuget:?package=Mesch.Authzn&version=0.0.1
#tool nuget:?package=Mesch.Authzn&version=0.0.1
Mesch.Authzn
A lightweight, role-based access control (RBAC) with optional ABAC (attribute-based access control) authorization library for .NET applications.
Overview
Mesch.Authzn provides a drop-in authorization engine that answers a single question: "Can this principal perform this action in this scope, right now?" The library ships with in-memory stores for immediate use and exposes interfaces for custom persistence implementations.
The authorization model is purely additive. Permissions are granted through roles, and roles are assigned to principals. No deny rules or negative permissions are supported.
Installation
dotnet add package Mesch.Authzn
Core Concepts
Principal
An actor in the system, represented by a PrincipalId. Principals can be users, services, or groups. All identity metadata (name, email, department) is maintained externally in the identity system.
Role
A reusable definition of authority. Roles contain permission grants and are stable, timeless definitions. A role does not change based on time or context.
Permission
A named action such as invoice.read or project.delete. Permissions support wildcard notation with the .* suffix (e.g., invoice.* matches invoice.read, invoice.write, etc.).
Scope
A key-value collection that constrains where a permission applies. Common scope keys include tenant, project, or resource. An empty scope applies universally.
Assignment
The link between a principal and a role. Assignments are the only time-varying element in the authorization model. An assignment can:
- Start in the future via
NotBefore - Expire via
NotAfter - Be revoked via the
Revokedflag
Authorization Decision
The result of an evaluation. Contains:
IsAllowed- Boolean indicating whether access is grantedDenyReason- Enumeration specifying why access was deniedMatchedRole- The role that granted permission (if allowed)MatchedPermission- The specific permission that was matched (if allowed)
Authorization Rule
A principal may perform an action if they hold an active assignment to a role that grants the required permission in the requested scope, and any attribute-based conditions evaluate to true.
Deny Reasons
The DenyReason enumeration provides diagnostic information:
None- Access allowedNoAssignments- Principal has no role assignmentsNoMatchingPermission- Principal has roles, but none grant the requested permissionScopeMismatch- Permission exists but scope does not matchAssignmentNotActive- Assignment is expired, not yet valid, or revokedAttributeEvaluationFailed- ABAC condition returned false or threw an exception
Basic Usage
Simple Authorization
var auth = AuthorizationBuilder.Create()
.AddRole("role:reader", r => r.Grant("invoice.read"))
.Assign("user:42", "role:reader")
.Build();
var decision = await auth.Engine
.For("user:42")
.On("invoice.read")
.EvaluateAsync();
if (decision.IsAllowed)
{
// Proceed with action
}
Wildcard Permissions
var auth = AuthorizationBuilder.Create()
.AddRole("role:admin", r => r.Grant("invoice.*"))
.Assign("user:1", "role:admin")
.Build();
// Matches invoice.read, invoice.write, invoice.delete, etc.
var decision = await auth.Engine
.For("user:1")
.On("invoice.delete")
.EvaluateAsync();
Scoped Permissions
var auth = AuthorizationBuilder.Create()
.AddRole("role:tenant-admin", r =>
r.Grant("invoice.*", new ScopeBag { ["tenant"] = "acme" }))
.Assign("user:99", "role:tenant-admin")
.Build();
// Allowed - scope matches
var decision1 = await auth.Engine
.For("user:99")
.On("invoice.read")
.InScope(new ScopeBag { ["tenant"] = "acme" })
.EvaluateAsync();
// Denied - scope mismatch
var decision2 = await auth.Engine
.For("user:99")
.On("invoice.read")
.InScope(new ScopeBag { ["tenant"] = "other" })
.EvaluateAsync();
Hierarchical Scopes
Granted scopes apply to equal or more specific requested scopes:
var auth = AuthorizationBuilder.Create()
.AddRole("role:project-admin", r =>
r.Grant("task.manage", new ScopeBag
{
["tenant"] = "acme",
["project"] = "alpha"
}))
.Assign("user:200", "role:project-admin")
.Build();
// Allowed - more specific scope
var decision = await auth.Engine
.For("user:200")
.On("task.manage")
.InScope(new ScopeBag
{
["tenant"] = "acme",
["project"] = "alpha",
["sprint"] = "sprint-1"
})
.EvaluateAsync();
Attribute-Based Access Control (ABAC)
Permission grants can include runtime conditions:
var auth = AuthorizationBuilder.Create()
.AddRole("role:approver", r =>
r.Grant(
"invoice.approve",
new ScopeBag { ["tenant"] = "acme" },
attrs =>
{
var amount = Convert.ToDecimal(attrs["amount"]);
var level = Convert.ToInt32(attrs["managerLevel"]);
return level >= 3 && amount <= 100000;
}))
.Assign("user:77", "role:approver")
.Build();
var decision = await auth.Engine
.For("user:77")
.On("invoice.approve")
.InScope(new ScopeBag { ["tenant"] = "acme" })
.WithAttributes(new AttributeBag
{
["amount"] = 50000m,
["managerLevel"] = 3
})
.EvaluateAsync();
Time-Bounded Assignments
var startDate = DateTimeOffset.UtcNow;
var endDate = startDate.AddDays(30);
var auth = AuthorizationBuilder.Create()
.AddRole("role:contractor", r => r.Grant("project.read"))
.Assign("user:50", "role:contractor", notBefore: startDate, notAfter: endDate)
.Build();
Revocation
var auth = AuthorizationBuilder.Create()
.AddRole("role:editor", r => r.Grant("document.edit"))
.Assign("user:25", "role:editor")
.Build();
// Later, revoke access
auth.Revoke("user:25", "role:editor");
Dependency Injection
Registration
services.AddAuthorizationEngine(builder =>
{
builder
.AddRole("role:admin", r => r.Grant("system.*"))
.AddRole("role:reader", r => r.Grant("system.read"))
.Assign("user:1", "role:admin");
});
Usage in Services
public class InvoiceService
{
private readonly IAuthorizationEngine _authEngine;
public InvoiceService(IAuthorizationEngine authEngine)
{
_authEngine = authEngine;
}
public async Task<Invoice> GetInvoiceAsync(string principalId, string invoiceId)
{
var decision = await _authEngine
.For(principalId)
.On("invoice.read")
.InScope(new ScopeBag { ["tenant"] = GetTenantForInvoice(invoiceId) })
.EvaluateAsync();
if (!decision.IsAllowed)
{
throw new UnauthorizedAccessException($"Access denied: {decision.DenyReason}");
}
return await LoadInvoiceAsync(invoiceId);
}
}
Custom Persistence
The library provides IRoleStore and IAssignmentStore interfaces for custom persistence implementations.
Interface Definitions
public interface IRoleStore
{
Task<Role?> GetRoleAsync(RoleId id, CancellationToken ct = default);
}
public interface IAssignmentStore
{
Task<IReadOnlyList<Assignment>> GetAssignmentsForPrincipalAsync(
PrincipalId principal, CancellationToken ct = default);
}
Entity Framework Core Example
public class EfCoreRoleStore : IRoleStore
{
private readonly AuthorizationDbContext _context;
public EfCoreRoleStore(AuthorizationDbContext context)
{
_context = context;
}
public async Task<Role?> GetRoleAsync(RoleId id, CancellationToken ct = default)
{
var entity = await _context.Roles
.Include(r => r.Grants)
.FirstOrDefaultAsync(r => r.Id == id.Value, ct);
if (entity == null)
{
return null;
}
var grants = entity.Grants.Select(g => new PermissionGrant(
g.Permission,
JsonSerializer.Deserialize<ScopeBag>(g.ScopeJson),
null // Conditions cannot be persisted
)).ToList();
return new Role(entity.Id, entity.Name, grants);
}
}
public class EfCoreAssignmentStore : IAssignmentStore
{
private readonly AuthorizationDbContext _context;
public EfCoreAssignmentStore(AuthorizationDbContext context)
{
_context = context;
}
public async Task<IReadOnlyList<Assignment>> GetAssignmentsForPrincipalAsync(
PrincipalId principal, CancellationToken ct = default)
{
var entities = await _context.Assignments
.Where(a => a.PrincipalId == principal.Value)
.ToListAsync(ct);
return entities.Select(e => new Assignment(
e.PrincipalId,
e.RoleId,
e.NotBefore,
e.NotAfter
)).ToList();
}
}
Registration with Custom Stores
services.AddDbContext<AuthorizationDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddSingleton<IRoleStore, EfCoreRoleStore>();
services.AddSingleton<IAssignmentStore, EfCoreAssignmentStore>();
services.AddAuthorizationEngine(
serviceProvider.GetRequiredService<IRoleStore>(),
serviceProvider.GetRequiredService<IAssignmentStore>());
Runtime Management
The AuthorizationHost provides convenience methods for in-memory store manipulation:
var host = AuthorizationBuilder.Create().Build();
// Add role at runtime
var newRole = new Role("role:analyst", "Data Analyst", new List<PermissionGrant>
{
new PermissionGrant("report.read"),
new PermissionGrant("report.export")
});
host.AddRole(newRole);
// Add assignment at runtime
host.AddAssignment(new Assignment("user:new", "role:analyst"));
// Revoke assignment at runtime
host.Revoke("user:old", "role:analyst");
These methods only work with InMemoryRoleStore and InMemoryAssignmentStore. They throw InvalidOperationException when custom stores are used.
Architecture
Evaluation Flow
- Retrieve all assignments for the principal
- Filter assignments to only those active at the current time
- For each active assignment:
- Retrieve the role definition
- Check each permission grant in the role:
- Match permission (exact or wildcard)
- Match scope (granted scope must be subset of requested scope)
- Evaluate ABAC condition if present
- Return allowed on first match
- If no matches found, determine appropriate deny reason
Permission Matching
Exact match:
Granted: "invoice.read"
Requested: "invoice.read"
Result: Match
Wildcard match:
Granted: "invoice.*"
Requested: "invoice.read"
Result: Match
No match:
Granted: "invoice.*"
Requested: "project.read"
Result: No match
Scope Matching
All keys in the granted scope must exist in the requested scope with matching values. Additional keys in the requested scope are permitted.
Granted: { "tenant": "acme" }
Requested: { "tenant": "acme", "project": "alpha" }
Result: Match
Granted: { "tenant": "acme", "project": "alpha" }
Requested: { "tenant": "acme" }
Result: No match (missing required key "project")
ABAC Evaluation
Conditions are evaluated after permission and scope matching. If a condition throws an exception or returns false, the grant is not applied and evaluation continues with the next grant. If no grants match after condition evaluation, DenyReason.AttributeEvaluationFailed is returned.
Design Constraints
Limitations
- No deny rules or negative permissions
- No direct permissions on principals (all permissions flow through roles)
- ABAC conditions cannot be persisted (they are code-based delegates)
- No built-in audit logging
- No permission inheritance or role hierarchies
- No query language or DSL for permission expressions
Performance Considerations
- In-memory stores perform linear scans
- Each authorization check loads all assignments for a principal
- Each assignment requires a role lookup
- Consider caching role definitions in custom stores
- Consider indexing principal assignments in custom stores
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.