D4S.RBAC 1.0.3

dotnet add package D4S.RBAC --version 1.0.3
                    
NuGet\Install-Package D4S.RBAC -Version 1.0.3
                    
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="D4S.RBAC" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="D4S.RBAC" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="D4S.RBAC" />
                    
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 D4S.RBAC --version 1.0.3
                    
#r "nuget: D4S.RBAC, 1.0.3"
                    
#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 D4S.RBAC@1.0.3
                    
#: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=D4S.RBAC&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=D4S.RBAC&version=1.0.3
                    
Install as a Cake Tool

D4S.RBAC

Role-Based Access Control (RBAC) management library for .NET applications. Provides a complete system for managing users, roles, permissions, and groups, with support for permission inheritance through the groups → roles → users hierarchy.

Table of Contents

Core concepts

The system is built around four entities:

Entity Description
User Represents a user identified by UPN (User Principal Name, e.g. john.doe@company.com)
Role An assignable role for users or groups (e.g. Administrator, Operator)
Permission An atomic permission assignable to users, roles, or groups (e.g. documents.read, users.edit)
Group A set of users to which roles and permissions can be collectively assigned

Setup

1. Install the NuGet package

dotnet add package D4S.RBAC

2. Implement IRbacUserContext

The library needs to know who the current user is. Implement this interface in the host project to provide the authenticated user's UPN:

public class CurrentUserContext(IHttpContextAccessor accessor) : IRbacUserContext
{
    public string Upn => accessor.HttpContext?.User.FindFirstValue("upn")
                      ?? accessor.HttpContext?.User.Identity?.Name
                      ?? string.Empty;
}

3. Register services in Program.cs

builder.Services.AddRbacService<CurrentUserContext>(builder.Configuration);

4. Database and connection string

By default the library uses the host application's DefaultConnection — the same database as the app, no additional configuration needed.

"ConnectionStrings": {
  "DefaultConnection": "Server=myserver;Database=mydb;Trusted_Connection=True;"
}

RBAC tables live in the rbac schema (rbac.RbacUsers, rbac.RbacRoles, etc.), isolated from the host application tables that typically use dbo. The migration history is also kept separate in rbac.__RbacMigrationsHistory — no conflicts with the host app migrations.

To use a dedicated database, specify a different connection string name in the options:

builder.Services.AddRbacService<CurrentUserContext>(builder.Configuration, options =>
{
    options.ConnectionStringName = "RbacConnection"; // dedicated connection string
});
"ConnectionStrings": {
  "DefaultConnection": "Server=myserver;Database=myappdb;...",
  "RbacConnection":    "Server=myserver;Database=myrbacdb;..."
}

Migrations are embedded in the package and applied automatically on application startup — no dotnet ef commands needed in the host project.

PostgreSQL

To use PostgreSQL instead of SQL Server, set the DatabaseProvider option:

builder.Services.AddRbacService<CurrentUserContext>(builder.Configuration, options =>
{
    options.DatabaseProvider = RbacDatabaseProvider.PostgreSql;
});

Or via appsettings.json:

"Rbac": {
  "DatabaseProvider": "PostgreSql"
}

The connection string format changes accordingly:

"ConnectionStrings": {
  "DefaultConnection": "Host=myserver;Database=mydb;Username=myuser;Password=mypassword"
}

The DatabaseProvider setting defaults to SqlServer. The value is resolved at startup, so it can also be driven by environment variables (e.g. Rbac__DatabaseProvider=PostgreSql).

Managing RBAC users

Key concept: to assign roles and permissions to a user, that user must exist in the RBAC database. An RBAC user is identified by their UPN (typically the email or username from the authentication system).

There are two scenarios depending on how the host application manages its own users.

Scenario A — The system has its own users table

If the host application already manages its own users (e.g. a Users table in the application database), the RBAC user must be created at the same time as the user in the main system. This keeps both systems always in sync.

public class UserService(IMyDbContext db, IRbacUsersService rbacUsers)
{
    public async Task CreateUserAsync(string email, string fullName)
    {
        // 1. Create the user in the main system
        var user = new AppUser { Email = email, FullName = fullName };
        db.Users.Add(user);
        await db.SaveChangesAsync();

        // 2. Create the RBAC user with the same UPN
        await rbacUsers.CreateUserAsync(new CreateRbacUserRequest(email));
    }
}

Similarly, when a user is deleted from the main system, they should also be deleted from RBAC:

await rbacUsers.DeleteUserAsync(rbacUserId);

Scenario B — The system does not manage users (e.g. external auth with Azure AD, Okta, etc.)

If the application has no user table of its own and delegates authentication to an external provider, the RBAC user should be created on first login. The right place to do this is a middleware or handler executed on every authenticated request:

app.Use(async (context, next) =>
{
    if (context.User.Identity?.IsAuthenticated == true)
    {
        var upn = context.User.FindFirstValue("upn") ?? context.User.Identity.Name;

        if (!string.IsNullOrEmpty(upn))
        {
            var rbacUsers = context.RequestServices.GetRequiredService<IRbacUsersService>();

            // Create the RBAC user only if they don't exist yet
            var existing = await rbacUsers.GetUserByUpnAsync(upn);
            if (existing is null)
                await rbacUsers.CreateUserAsync(new CreateRbacUserRequest(upn));
        }
    }

    await next();
});

This way, on first login the user is automatically registered in the RBAC system, ready to receive roles and permissions.

Usage modes

Once RBAC users exist, roles and permissions can be configured in two ways, which are not mutually exclusive.

Static mode — manual configuration

Suitable for applications where roles and permissions are predefined and don't change frequently. Configuration is done via a seed run at startup:

public static class RbacSeeder
{
    public static async Task SeedAsync(IServiceProvider services)
    {
        using var scope = services.CreateScope();
        var roles = scope.ServiceProvider.GetRequiredService<IRbacRolesService>();
        var permissions = scope.ServiceProvider.GetRequiredService<IRbacPermissionsService>();
        var users = scope.ServiceProvider.GetRequiredService<IRbacUsersService>();

        // Create permissions
        var readId = await permissions.CreatePermissionAsync(new("documents.read", "Read documents"));
        var writeId = await permissions.CreatePermissionAsync(new("documents.write", "Write documents"));

        // Create roles
        var adminId = await roles.CreateRoleAsync(new("Administrator", "Full access"));
        var operatorId = await roles.CreateRoleAsync(new("Operator", "Read-only access"));

        // Assign permissions to roles
        await roles.AssignPermissionToRoleAsync(adminId, readId);
        await roles.AssignPermissionToRoleAsync(adminId, writeId);
        await roles.AssignPermissionToRoleAsync(operatorId, readId);

        // Assign role to a user (must already exist in the RBAC db)
        var user = await users.GetUserByUpnAsync("john.doe@company.com");
        if (user is not null)
            await users.AssignRoleToUserAsync(user.Id, adminId);
    }
}
// In Program.cs
await RbacSeeder.SeedAsync(app.Services);

Dynamic mode — REST endpoints

Suitable for applications that need to manage roles and permissions through a user interface (admin panel, back-office, etc.).

Register the endpoints in Program.cs:

app.MapRbacEndpoints();

This exposes a set of REST endpoints under the /rbac prefix (configurable), all protected by authorization. The endpoints are automatically documented in Swagger.

Endpoint protection: by default the rbac.admin permission is required. The first admin user must therefore be created via seed (static mode) or directly in the database, after which they can manage everything through the endpoints.

The available endpoints cover all CRUD operations on users, roles, permissions, and groups, as well as managing associations between them.

Granularity: roles vs permissions

The system supports three approaches; choose the one that fits the application's complexity.

Roles only

Suitable when authorization is simple and coarse-grained: each role represents a well-defined access level and there is no need to distinguish between individual features.

// Seed: create roles without associated permissions
var adminId = await roles.CreateRoleAsync(new("Administrator", "Full access"));
var operatorId = await roles.CreateRoleAsync(new("Operator", "Read-only access"));

// Check in application code
if (!await auth.HasRoleAsync(user.Upn, "Administrator"))
    return Results.Forbid();

Permissions only

Suitable when application features have differentiated and independent access requirements. Permissions are assigned directly to users or grouped into roles for convenience.

// Seed: create atomic permissions and assign them to roles
var readId = await permissions.CreatePermissionAsync(new("documents.read", "Read documents"));
var writeId = await permissions.CreatePermissionAsync(new("documents.write", "Write documents"));
await roles.AssignPermissionToRoleAsync(adminId, readId);
await roles.AssignPermissionToRoleAsync(adminId, writeId);

// Check in application code
if (!await auth.HasPermissionAsync(user.Upn, "documents.write"))
    return Results.Forbid();

Mixed approach

HasRoleAsync and HasPermissionAsync can be used in different parts of the same application. A clear and consistent convention is recommended to prevent authorization logic from becoming hard to reason about.

Protecting RBAC endpoints

The same choice applies to protecting the management endpoints exposed by MapRbacEndpoints. By default the rbac.admin permission is checked, but a role can be used instead:

builder.Services.AddRbacService<CurrentUserContext>(builder.Configuration, options =>
{
    // Permission-based approach (default)
    options.AdminPermission = "rbac.admin";

    // Or: role-based approach
    options.AdminRole = "Administrator";

    // Or: ASP.NET Core policy (takes precedence over both)
    options.AdminPolicy = "AdminOnly";
});

Precedence: AdminPolicyAdminRoleAdminPermission.

Checking permissions

To protect host application features, inject IRbacAuthorizationService:

public class DocumentService(IRbacAuthorizationService auth, IRbacUserContext user)
{
    public async Task<Document> GetDocumentAsync(Guid id)
    {
        if (!await auth.HasPermissionAsync(user.Upn, "documents.read"))
            throw new UnauthorizedAccessException();

        // ...
    }
}

Or directly in endpoints:

app.MapGet("/documents", async (IRbacAuthorizationService auth, IRbacUserContext user, IDocumentService svc) =>
{
    if (!await auth.HasPermissionAsync(user.Upn, "documents.read"))
        return Results.Forbid();

    return Results.Ok(await svc.GetAllAsync());
});

Filters for host project endpoints

In addition to calling IRbacAuthorizationService manually, host project endpoints can be protected with the filters provided by the library, applicable to both individual endpoints and entire groups.

Single endpoint

app.MapGet("/documents", handler)
   .RequireRbacPermission("documents.read");

app.MapDelete("/documents/{id}", handler)
   .RequireRbacPermission("documents.write");

app.MapGet("/admin/dashboard", handler)
   .RequireRbacRole("Administrator");

Endpoint group

var adminGroup = app.MapGroup("/admin")
    .RequireRbacRole("Administrator");

adminGroup.MapGet("/users", ...);
adminGroup.MapPost("/users", ...);
// all endpoints in the group require the "Administrator" role
var docsGroup = app.MapGroup("/documents")
    .RequireRbacPermission("documents.read");

docsGroup.MapGet("/", ...);
docsGroup.MapGet("/{id}", ...);

Permission inheritance

Permissions are resolved by traversing the entire hierarchy. Given the following assignments:

User john.doe
  ├── Direct permission: "report.export"
  ├── Role: "Operator"
  │     └── Permission: "documents.read"
  └── Group: "Sales Team"
        ├── Direct permission: "crm.access"
        └── Role: "Supervisor"
              └── Permission: "crm.edit"

GetUserPermissionsAsync will return: report.export, documents.read, crm.access, crm.edit — without duplicates.

Advanced configuration

builder.Services.AddRbacService<CurrentUserContext>(builder.Configuration, options =>
{
    // Route prefix for the endpoints (default: "rbac")
    options.RoutePrefix = "api/rbac";

    // Requires ASP.NET Core authentication before the RBAC check (default: true)
    // Disable only if authentication is handled elsewhere
    options.RequireAuthentication = true;

    // RBAC permission required to access the management endpoints (default: "rbac.admin")
    options.AdminPermission = "rbac.admin";

    // Alternative: RBAC role required instead of a permission
    // If set, takes precedence over AdminPermission
    options.AdminRole = "Administrator";

    // Alternative: ASP.NET Core policy instead of the internal RBAC check
    // If set, takes precedence over AdminRole and AdminPermission
    options.AdminPolicy = "AdminOnly";

    // Database provider (default: SqlServer). Use PostgreSql to target PostgreSQL.
    options.DatabaseProvider = RbacDatabaseProvider.PostgreSql;
});

Precedence: AdminPolicyAdminRoleAdminPermission.

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.

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
1.0.3 40 5/20/2026
1.0.2 87 5/18/2026
1.0.1 99 5/8/2026
1.0.0 88 5/7/2026