Skip to content

Repository files navigation

Privileged

Privileged is an authorization library for restricting resources by action, subject and qualifiers. It's designed to be incrementally adoptable and can easily scale between a simple claim based and fully featured subject and action based authorization. It makes it easy to manage and share permissions across UI components, API services, and database queries.

Build ProjectLicenseCoverage Status

PackageVersionDescription
PrivilegedNuGetCore authorization library for rule-based permissions
Privileged.AuthorizationNuGetASP.NET Core authorization integration with attribute-based policies
Privileged.ComponentsNuGetBlazor components for privilege-aware UI elements
Privileged.EndpointNuGetASP.NET Core endpoint extensions for privilege requirements

Installation

Install the core package via NuGet:

dotnet add package Privileged

For ASP.NET Core applications with attribute-based authorization, also install the authorization package:

dotnet add package Privileged.Authorization

For ASP.NET Core applications using minimal APIs with privilege requirements, also install the endpoint package:

dotnet add package Privileged.Endpoint

For Blazor applications, also install the components package:

dotnet add package Privileged.Components

Features

  • Versatile An incrementally adoptable and can easily scale between a simple claim based and fully featured subject and attribute based authorization.
  • Isomorphic Can be used on front-end and back-end and complementary packages make integration with Frontend and Backend effortless
  • Declarative Thanks to declarative rules, you can serialize and share permissions between UI and API or microservices
  • Rule-based Support for both allow and forbid rules with rule precedence
  • Aliases Create reusable aliases for actions, subjects, and qualifiers
  • Qualifiers Fine-grained control with field-level permissions
  • ASP.NET Core Integration Seamless integration with ASP.NET Core authorization using attribute-based policies
  • Blazor Integration Ready-to-use components for conditional rendering and privilege-aware form inputs
  • Performance Optimized Efficient rule evaluation and matching algorithms

General

Privileged operates on rules for what a user can actually do in the application. A rule itself depends on the 3 parameters:

  1. Action Describes what user can actually do in the app. User action is a word (usually a verb) which depends on the business logic (e.g., update, read). Very often it will be a list of words from CRUD - create, read, update and delete.
  2. Subject The subject which you want to check user action on. Usually this is a business (or domain) entity name (e.g., Subscription, Post, User).
  3. Qualifiers Can be used to restrict user action only to matched subject's qualifiers (e.g., to allow moderator to update published field of Post but not update description or title)

Basic Usage

Simple Rules

Using builder to create basic allow and forbid rules:

varcontext=newPrivilegeBuilder().Allow("read","Post").Allow("write","User").Forbid("delete","User").Build();// Check permissionsboolcanReadPost=context.Allowed("read","Post");// trueboolcanWriteUser=context.Allowed("write","User");// trueboolcanDeleteUser=context.Allowed("delete","User");// falseboolcanReadUser=context.Allowed("read","User");// false (not explicitly allowed)

Wildcard Rules

Use wildcards to allow all actions on a subject or an action on all subjects:

varcontext=newPrivilegeBuilder().Allow("test",PrivilegeRule.Any)// Allow 'test' action on any subject.Allow(PrivilegeRule.Any,"Post")// Allow any action on 'Post'.Forbid("publish","Post")// Forbid overrides allow.Build();context.Allowed("read","Post").Should().BeTrue();context.Allowed("update","Post").Should().BeTrue();context.Allowed("archive","Post").Should().BeTrue();context.Allowed("read","User").Should().BeFalse();context.Allowed("delete","Post").Should().BeTrue();context.Allowed("publish","Post").Should().BeFalse();// Forbid takes precedencecontext.Allowed("test","User").Should().BeTrue();context.Allowed("test","Post").Should().BeTrue();

Using Qualifiers

Qualifiers provide field-level or fine-grained permissions:

varcontext=newPrivilegeBuilder().Allow("read","Post",["title","id"])// Only allow reading specific fields.Allow("read","User")// Allow reading all User fields.Build();// Post permissions with qualifierscontext.Allowed("read","Post").Should().BeTrue();// General permissioncontext.Allowed("read","Post","id").Should().BeTrue();// Specific field allowedcontext.Allowed("read","Post","title").Should().BeTrue();// Specific field allowedcontext.Allowed("read","Post","content").Should().BeFalse();// Field not allowed// User permissions without qualifierscontext.Allowed("read","User").Should().BeTrue();// All fields allowedcontext.Allowed("read","User","id").Should().BeTrue();// Any field allowed

Advanced Features

Multiple Actions and Subjects

Use extension methods for bulk rule creation:

varcontext=newPrivilegeBuilder().Allow(["read","update"],"Post")// Multiple actions, single subject.Allow("read",["Post","User"])// Single action, multiple subjects.Allow(["create","read"],["Post","Comment"])// Multiple actions and subjects.Build();context.Allowed("read","Post").Should().BeTrue();context.Allowed("update","Post").Should().BeTrue();context.Allowed("read","User").Should().BeTrue();context.Allowed("create","Comment").Should().BeTrue();

Aliases

Create reusable aliases for common groupings:

varcontext=newPrivilegeBuilder().Alias("Manage",["Create","Update","Delete"],PrivilegeMatch.Action).Allow("Manage","Project")// Allows all actions defined in the "Manage" alias.Allow("Read","User")// Allows reading User.Allow("Update","User",["Profile","Settings"])// Allows updating User's Profile and Settings.Forbid("Delete","User")// Forbids deleting User.Build();boolcanCreateProject=context.Allowed("Create","Project");// trueboolcanReadUser=context.Allowed("Read","User");// trueboolcanUpdateProfile=context.Allowed("Update","User","Profile");// trueboolcanUpdatePassword=context.Allowed("Update","User","Password");// falseboolcanDeleteUser=context.Allowed("Delete","User");// false

Qualifier Aliases

Aliases can also be used for qualifiers:

varcontext=newPrivilegeBuilder().Alias("PublicFields",["title","summary","author"],PrivilegeMatch.Qualifier).Allow("read","Post",["PublicFields"]).Build();context.Allowed("read","Post","title").Should().BeTrue();context.Allowed("read","Post","summary").Should().BeTrue();context.Allowed("read","Post","content").Should().BeFalse();

Rule Evaluation

Rule Precedence

Rules are evaluated in the order they are defined, with more specific rules taking precedence:

  1. Forbid rules always take precedence over allow rules when both match
  2. Rules are matched based on exact string comparison
  3. Wildcard rules PrivilegeRule.Any match any value
  4. Alias expansion happens during rule matching

String Comparison

By default, rule matching uses StringComparer.InvariantCultureIgnoreCase. You can customize this:

varcontext=newPrivilegeContext(rules,aliases,StringComparer.Ordinal);

API Reference

PrivilegeBuilder

  • Allow(string action, string subject, IEnumerable<string>? qualifiers = null) - Add an allow rule
  • Forbid(string action, string subject, IEnumerable<string>? qualifiers = null) - Add a forbid rule
  • Alias(string alias, IEnumerable<string> values, PrivilegeMatch type) - Create an alias
  • Build() - Create the PrivilegeContext

PrivilegeContext

  • Allowed(string? action, string? subject, string? qualifier = null) - Check if action is allowed
  • Forbidden(string? action, string? subject, string? qualifier = null) - Check if action is forbidden
  • MatchRules(string? action, string? subject, string? qualifier = null) - Get matching rules

Extension Methods

  • Allow(IEnumerable<string> actions, string subject, ...) - Allow multiple actions on single subject
  • Allow(string action, IEnumerable<string> subjects, ...) - Allow single action on multiple subjects
  • Allow(IEnumerable<string> actions, IEnumerable<string> subjects, ...) - Allow multiple actions on multiple subjects
  • Similar Forbid overloads for forbid rules

ASP.NET Core Authorization Integration

The Privileged.Authorization package provides seamless integration with ASP.NET Core's authorization system through attribute-based policies.

Authorization Setup

First, configure the authorization services in your Program.cs:

// Program.csusingPrivileged.Authorization;varbuilder=WebApplication.CreateBuilder(args);// Add authentication here (JWT/Cookies/etc.)builder.Services.AddAuthentication(...);builder.Services.AddAuthorization();// Register privilege services + your providerbuilder.Services.AddPrivilegeAuthorization();builder.Services.AddScoped<IPrivilegeContextProvider,YourPrivilegeContextProvider>();varapp=builder.Build();app.UseAuthentication();app.UseAuthorization();varapp=builder.Build();

Using the PrivilegeAttribute

Use the [Privilege] attribute on controllers and actions to declaratively specify authorization requirements:

[ApiController][Route("api/[controller]")]publicclassPostsController:ControllerBase{[HttpGet][Privilege("read","Post")]publicIActionResultGetPosts(){// Only users with "read" privilege on "Post" can access thisreturnOk();}[HttpPost][Privilege("create","Post")]publicIActionResultCreatePost([FromBody]CreatePostRequestrequest){// Only users with "create" privilege on "Post" can access thisreturnOk();}[HttpPut("{id}")][Privilege("update","Post")]publicIActionResultUpdatePost(intid,[FromBody]UpdatePostRequestrequest){// Only users with "update" privilege on "Post" can access thisreturnOk();}[HttpDelete("{id}")][Privilege("delete","Post")]publicIActionResultDeletePost(intid){// Only users with "delete" privilege on "Post" can access thisreturnNoContent();}[HttpPut("{id}/title")][Privilege("update","Post","title")]publicIActionResultUpdatePostTitle(intid,[FromBody]stringtitle){// Only users with "update" privilege on "Post" for "title" field can access thisreturnOk();}}

Manual Authorization Checks

You can also perform manual authorization checks in your controllers:

[ApiController][Route("api/[controller]")]publicclassPostsController:ControllerBase{privatereadonlyIPrivilegeContextProvider_contextProvider;publicPostsController(IPrivilegeContextProvidercontextProvider){_contextProvider=contextProvider;}[HttpGet("{id}")]publicasyncTask<IActionResult>GetPost(intid){varcontext=await_contextProvider.GetContextAsync();if(!context.Allowed("read","Post")){returnForbid();}// Additional business logic...returnOk();}[HttpPut("{id}")]publicasyncTask<IActionResult>UpdatePost(intid,[FromBody]UpdatePostRequestrequest){varcontext=await_contextProvider.GetContextAsync();// Check different permissions based on what's being updatedif(!string.IsNullOrEmpty(request.Title)&&!context.Allowed("update","Post","title")){returnForbid("Cannot update post title");}if(!string.IsNullOrEmpty(request.Content)&&!context.Allowed("update","Post","content")){returnForbid("Cannot update post content");}// Perform update...returnOk();}}

Minimal API Example

You can also use privilege-based authorization with ASP.NET Core Minimal APIs. The [Privilege] attribute works on route handler delegates, and the dynamic policies will be generated in exactly the same way.

For minimal APIs that need to use the RequirePrivilege extension method, also add the Privileged.Endpoint package.

usingMicrosoft.AspNetCore.Authorization;usingPrivileged.Authorization;usingPrivileged.Endpoint;varbuilder=WebApplication.CreateBuilder(args);// Add authentication here (JWT/Cookies/etc.)builder.Services.AddAuthentication(...);builder.Services.AddAuthorization();// Register privilege services + your providerbuilder.Services.AddPrivilegeAuthorization<DatabasePrivilegeContextProvider>();varapp=builder.Build();app.UseAuthentication();app.UseAuthorization();// Simple collection endpoint requiring read privilege on Postapp.MapGet("/api/posts",[Privilege("read","Post")]()=>Results.Ok(new[]{new{Id=1,Title="Hello"}}));// Create endpoint requiring create privilegeapp.MapPost("/api/posts",[Privilege("create","Post")](CreatePostRequestreq)=>{// Business logic...returnResults.Created($"/api/posts/{123}",req);});// Update with qualifier (field-level) example using RequirePrivilege extensionapp.MapPut("/api/posts/{id}/title",(intid,stringtitle)=>{// Only users with update privilege on Post:title reach herereturnResults.Ok();}).RequirePrivilege("update","Post","title");// Manual check example inside a handlerapp.MapPut("/api/posts/{id}",async(intid,UpdatePostRequestreq,IPrivilegeContextProviderprovider)=>{varcontext=awaitprovider.GetContextAsync();if(req.Titleis not null&&!context.Allowed("update","Post","title"))returnResults.Forbid();if(req.Contentis not null&&!context.Allowed("update","Post","content"))returnResults.Forbid();returnResults.Ok();});app.Run();// Example request modelspublicrecordCreatePostRequest(stringTitle,stringContent);publicrecordUpdatePostRequest(string?Title,string?Content);

Key points:

  • Apply [Privilege] directly to route handler delegates.
  • Dynamic policy names follow the Privilege:action:subject[:qualifier] format automatically.
  • For ad-hoc logic or multiple field checks, inject IPrivilegeContextProvider and perform manual Allowed calls.
  • Combine with your existing authentication middleware (JWT, cookies, etc.).

IPrivilegeContextProvider Implementation

The IPrivilegeContextProvider interface allows you to load privilege contexts asynchronously, which is useful for scenarios where permissions are loaded from external sources like APIs, databases, or authentication systems. This interface is used by both ASP.NET Core and Blazor applications.

ASP.NET Core: Database-Based Provider

For ASP.NET Core applications that load permissions from a database or external service.

publicclassDatabasePrivilegeContextProvider:IPrivilegeContextProvider{privatereadonlyIUserPermissionService_permissionService;privatereadonlyHybridCache_cache;privatereadonlyILogger<DatabasePrivilegeContextProvider>_logger;publicDatabasePrivilegeContextProvider(IUserPermissionServicepermissionService,HybridCachecache,ILogger<DatabasePrivilegeContextProvider>logger){_permissionService=permissionService;_cache=cache;_logger=logger;}publicasyncValueTask<PrivilegeContext>GetContextAsync(ClaimsPrincipal?claimsPrincipal=null){varuser=claimsPrincipal;// Return empty context for unauthenticated usersif(user?.Identity?.IsAuthenticated!=true)returnPrivilegeContext.Empty;try{varuserId=user.FindFirst(ClaimTypes.NameIdentifier)?.Value;if(userId==null){_logger.LogWarning("User ID not found in claims");returnPrivilegeContext.Empty;}stringcacheKey=$"privileged:permissions:{userId}";// Cache the privilege model to avoid repeated DB/service calls per request.PrivilegeModelprivilegeModel=await_cache.GetOrCreateAsync(cacheKey,async ct =>await_permissionService.GetUserPermissionsAsync(userId),
options =>options.SetExpiration(TimeSpan.FromMinutes(5)));returnnewPrivilegeContext(privilegeModel);}catch(Exceptionex){_logger.LogError(ex,"Failed to load privileges for current user");returnPrivilegeContext.Empty;}}}

Blazor: API-Based Provider

For Blazor applications that load permissions from an API:

publicclassHttpPrivilegeContextProvider:IPrivilegeContextProvider{privatereadonlyHttpClient_httpClient;privatereadonlyILogger<HttpPrivilegeContextProvider>_logger;publicHttpPrivilegeContextProvider(HttpClienthttpClient,ILogger<HttpPrivilegeContextProvider>logger){_httpClient=httpClient;_logger=logger;}publicasyncValueTask<PrivilegeContext>GetContextAsync(ClaimsPrincipal?claimsPrincipal=null){try{// Load privilege model from APIvarprivilegeModel=await_httpClient.GetFromJsonAsync<PrivilegeModel>("/api/user/privileges");returnnewPrivilegeContext(privilegeModel);}catch(Exceptionex){_logger.LogError(ex,"Failed to load user privileges");// Return minimal context with basic read permissions as fallbackreturnnewPrivilegeBuilder().Allow("read","Public").Build();}}}

Static Provider

For simpler scenarios with static permissions:

publicclassStaticPrivilegeContextProvider:IPrivilegeContextProvider{publicValueTask<PrivilegeContext>GetContextAsync(ClaimsPrincipal?claimsPrincipal=null){varcontext=newPrivilegeBuilder().Allow("read","Post").Allow("write","Post",new[]{"title","content"}).Allow("delete","Post").Forbid("publish","Post")// Override specific action.Build();returnValueTask.FromResult(context);}}

Registration

Register your chosen provider in Program.cs:

// For ASP.NET Corebuilder.Services.AddScoped<IPrivilegeContextProvider,DatabasePrivilegeContextProvider>();// For Blazorbuilder.Services.AddScoped<IPrivilegeContextProvider,HttpPrivilegeContextProvider>();// orbuilder.Services.AddScoped<IPrivilegeContextProvider,StaticPrivilegeContextProvider>();

Blazor Integration

The Privileged.Components package provides components for conditional rendering based on permissions.

Blazor Setup

First, add the privilege context as a cascading value in your app:

// Program.cs or similar// Create privilege rulesvarprivilegeContext=newPrivilegeBuilder().Allow("read","Post").Allow("edit","Post",["title","content"]).Allow("delete","Post").Build();// Make available as cascading parameterbuilder.Services.AddCascadingValue(_ =>privilegeContext);

PrivilegeContextView Component

For scenarios where you need to load the privilege context asynchronously, use the PrivilegeContextView component:

<PrivilegeContextView>
<Loading>
<divclass="spinner-border"role="status">
<spanclass="visually-hidden">Loading permissions...</span>
</div>
</Loading>
<Loaded>
<PrivilegeViewAction="read"Subject="Post">
<p>Content loaded with permissions!</p>
</PrivilegeView>
</Loaded>
</PrivilegeContextView>

PrivilegeContextView component requires an IPrivilegeContextProvider service to be registered:

// In Program.csbuilder.Services.AddScoped<IPrivilegeContextProvider,YourPrivilegeContextProvider>();

Using PrivilegeContextView in a Layout

The most common pattern is to wrap your entire layout with PrivilegeContextView to ensure permissions are loaded before any page content is rendered:

@* MainLayout.razor *@@inherits LayoutComponentBase
<PrivilegeContextView>
<divclass="page">
<divclass="sidebar">
<NavMenu />
</div>
<main>
@Body
</main>
</div>
</PrivilegeContextView>

With this approach, all pages will automatically have access to the privilege context, and users will see a loading state until permissions are loaded. Your navigation menu can also use privilege checking:

PrivilegeView Component

Use the PrivilegeView component to conditionally render content:

@* Basic usage with ChildContent *@
<PrivilegeViewAction="read"Subject="Post">
<p>You can read posts!</p>
</PrivilegeView>
@* With both allowed and forbidden content *@
<PrivilegeViewAction="delete"Subject="Post">
<Allowed>
<buttonclass="btn btn-danger">Delete Post</button>
</Allowed>
<Forbidden>
<spanclass="text-muted">Delete not allowed</span>
</Forbidden>
</PrivilegeView>
@* With field-level permissions *@
<PrivilegeViewAction="edit"Subject="Post"Field="title">
<inputtype="text"placeholder="Edit title" />
</PrivilegeView>

PrivilegeLink Component

The PrivilegeLink component extends the standard NavLink component to provide privilege-aware navigation. It only renders the link when the user has the required permissions, making it perfect for building navigation menus and UI elements that should only be visible to authorized users.

The link components require a PrivilegeContext cascading parameter.

@* Basic navigation link that only shows if user can read posts *@
<PrivilegeLinkSubject="Post"Action="read"href="/posts">
View Posts
</PrivilegeLink>
@* Link with custom action *@
<PrivilegeLinkSubject="Post"Action="edit"href="/posts/edit">
Edit Posts
</PrivilegeLink>
@* Link with field-level permissions *@
<PrivilegeLinkSubject="Post"Action="update"Qualifier="title"href="/posts/edit-title">
Edit Post Titles
</PrivilegeLink>
@* Navigation menu example *@
<navclass="navbar">
<PrivilegeLinkSubject="Post"href="/posts"class="nav-link">
Posts
</PrivilegeLink>
<PrivilegeLinkSubject="User"Action="manage"href="/users"class="nav-link">
Users
</PrivilegeLink>
<PrivilegeLinkSubject="Settings"Action="edit"href="/settings"class="nav-link">
Settings
</PrivilegeLink>
</nav>
@* Using with CSS classes and additional attributes *@
<PrivilegeLinkSubject="Post"Action="delete"href="/posts/delete"class="btn btn-danger"@onclick="ConfirmDelete">
Delete Post
</PrivilegeLink>

Privilege-Aware Input Components

The Privileged.Components package also includes privilege-aware input components that automatically handle read/write permissions:

The input components require a PrivilegeContext cascading parameter.

@* Text input that becomes read-only based on permissions *@
<PrivilegeInputText@bind-Value="@model.Title"Subject="Post"Field="title"ReadAction="read"UpdateAction="update" />
@* Number input with privilege checking *@
<PrivilegeInputNumber@bind-Value="@model.Views"Subject="Post"Field="views" />
@* Select dropdown with privilege-based enabling/disabling *@
<PrivilegeInputSelect@bind-Value="@model.Status"Subject="Post"Field="status">
<optionvalue="draft">Draft</option>
<optionvalue="published">Published</option>
</PrivilegeInputSelect>
@* Checkbox with privilege checking *@
<PrivilegeInputCheckbox@bind-Value="@model.IsActive"Subject="Post"Field="isActive" />
@* Text area with privilege checking *@
<PrivilegeInputTextArea@bind-Value="@model.Content"Subject="Post"Field="content"rows="5" />

These components automatically:

  • Enable/disable based on update permissions
  • Show/hide based on read permissions

PrivilegeForm Component

The PrivilegeForm component extends the standard EditForm to provide privilege-aware form functionality. It automatically cascades privilege form state to child components, allowing you to set default privilege settings at the form level while maintaining the ability for individual components to override specific values.

@* Basic form with default privilege settings *@
<PrivilegeFormModel="@postModel"Subject="Post"ReadAction="read"UpdateAction="update">
<DataAnnotationsValidator />
<ValidationSummary />
@* These inputs inherit the Subject, ReadAction, and UpdateAction from the form *@
<PrivilegeInputText@bind-Value="@postModel.Title"Field="title" />
<PrivilegeInputTextArea@bind-Value="@postModel.Content"Field="content" />
<PrivilegeInputSelect@bind-Value="@postModel.Status"Field="status">
<optionvalue="draft">Draft</option>
<optionvalue="published">Published</option>
</PrivilegeInputSelect>
@* This input overrides the default Subject *@
<PrivilegeInputText@bind-Value="@postModel.AuthorEmail"Subject="User"Field="email" />
<buttontype="submit"class="btn btn-primary">Save Post</button>
</PrivilegeForm>
@* Form with different actions for different operations *@
<PrivilegeFormModel="@userModel"Subject="User"ReadAction="view"UpdateAction="edit">
<PrivilegeInputText@bind-Value="@userModel.FirstName"Field="firstName" />
<PrivilegeInputText@bind-Value="@userModel.LastName"Field="lastName" />
@* Admin-only field with different permissions *@
<PrivilegeInputText@bind-Value="@userModel.Role"Subject="User"Field="role"ReadAction="viewRole"UpdateAction="editRole" />
</PrivilegeForm>

Key benefits of PrivilegeForm:

  • Cascading Defaults: Set privilege parameters once at the form level instead of repeating them on every input
  • Flexible Overrides: Individual components can override any of the cascaded values when needed
  • Standard EditForm: Maintains all functionality of the base EditForm component including validation
  • Clean Markup: Reduces repetitive code and makes forms easier to maintain

PrivilegeInputText HTML Output Example

Below is an example of the PrivilegeInputText component and its corresponding HTML output for various states based on the PrivilegeContext results:

PrivilegeInputText Component

<PrivilegeInputText@bind-Value="@model.Title"Subject="Post"Field="title" />

Corresponding HTML Output

<!-- When the user has 'update' permission --><inputtype="text" id="Title" name="Title" value="Sample Title" /><!-- When the user has only 'read' permission, make input readonly --><inputtype="text" id="Title" name="Title" value="Sample Title" readonly/><!-- When the user has neither 'read' nor 'update' permission, use password type and disable --><inputtype="password" id="Title" name="Title" disabled/>

This demonstrates how the PrivilegeInputText component dynamically adapts its output based on the user's permissions as determined by the PrivilegeContext.

License

This project is licensed under the MIT License.

References

Inspired by CASL

About

Privileged is an authorization library for restricting resources by action, subject and qualifiers.

Topics

Resources

Stars

76 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages