RBAC Walkthrough: End-to-End Example

This walkthrough implements a complete RBAC setup from scratch: defining responsibilities, creating roles, assigning responsibilities to roles, and assigning roles to users.

Scenario

A contact management module needs two roles:

  • Salesperson — can create and edit contacts
  • Sales Manager — can do everything a salesperson can, plus view all contacts

Step 1: Define responsibilities in a Logic class

Responsibilities are defined in a [Logic] class using IResponsibilityRegistry. Define a method marked with [RegisterLogic] — this runs at startup and registers the responsibilities into the database.

Declare the responsibility names as class-level constants so they can be reused in other methods (such as the data generator in Step 2).

using Benevia.Core.Contacts.Model;
using Benevia.Core.Permissions;

namespace Benevia.ERP.Company.Contacts;

[Logic]
public class Permissions(IResponsibilityRegistry access)
{
    public static readonly string ViewContactsResponsibility = "View contacts";
    public static readonly string ModifyContactsResponsibility = "Modify contacts";

    [RegisterLogic]
    public void CreateContactResponsibilities()
    {
        access.AddResponsibility(ViewContactsResponsibility, "View and select contacts but cannot change any contact information", grant =>
        {
            grant.Entity<Contact>()
                .View()
                .ReadForAllProperties();
            grant.Entity<Address>()
                .View()
                .ReadForAllProperties();
            grant.Entity<Country>()
                .View()
                .ReadForAllProperties();
        });

        access.AddResponsibility(ModifyContactsResponsibility, "Create and modify contact information", grant =>
        {
            grant.Requires(Permissions.ViewContactsResponsibility); 

            //Add the following in addition to the 'View contacts' permissions
            grant.Entity<Contact>()
                .CreateAndDelete()
                .WriteForAllProperties();
            grant.Entity<Address>()
                .Create()
                .WriteForAllProperties();
            grant.Entity<Country>()
                .View()
                .ReadForAllProperties();
        });
    }
}

Step 2: Create roles and assign responsibilities

Create a data generator class that inherits from ICreateData and a [DataGenerator] method. BlankData runs before any user data is added, making it the right place to create roles that ship with the product.

using Benevia.Core.API.Communications.Authentication.Models;
using Benevia.Core.API.Database;
using Benevia.Core.API.Roles;
using Benevia.Core.DataGenerator;

public class RoleCreator : ICreateData
{
    [DataGenerator(DataKind.BlankData, 2)]
    public void AddReceptionistRole(IDataContext dataContext, EventContext context)
    {
        var role = new Role()
        {
            Name = "Receptionist",
            Description = "Can view and manage contacts"
        };
        dataContext.AddEntity(role);
        role.AddResponsibility(dataContext, Permissions.ViewContactsResponsibility);
        role.AddResponsibility(dataContext, Permissions.ModifyContactsResponsibility);
    }
}

Step 3: Assign a role to a user

Roles are assigned to users through the UserRole link entity, which can be accessed and modified like any other entity — through the API or in a data generator. In this example we set the order to 4 so that it runs after the above data creator method of 2.

using Benevia.Core.API.Communications.Authentication.Models;

[DataGenerator(DataKind.DemoData, 4)]
public void AssignRoles(IDataContext dataContext, EventContext context)
{
    var user = context.GetEntity<User>("alice@example.com");
    var role = context.GetEntity<Role>("Receptionist");
    
    var userRole = new UserRole
    {
        UserId = user.Id,
        User = user,
        RoleId = role.Id,
        Role = role
    };
    dataContext.AddEntity(userRole);
}

In production, role assignment is typically done through the UI or API rather than in a data generator.

How permissions resolve at runtime

When a request comes in, the system:

  1. Collects all roles attached to the user
  2. Collects all responsibilities from those roles
  3. For each entity/property/method, takes the most permissive setting across all responsibilities

If a user has multiple roles, he would get the union of all permissions from both roles' responsibilities. Since the most permissive setting wins, there is no harm in assigning both.

Adding a responsibility to an existing role

If another feature needs to add a responsibility to a role defined elsewhere, use a separate data generator with a higher priority number (higher = runs later):

using Benevia.Core.API.Communications.Authentication.Models;
using Benevia.Core.API.Roles;

[DataGenerator(DataKind.BlankData, 3)]
public void AddContactExportToReceptionist(IDataContext dataContext, EventContext context)
{
    var role = context.GetEntity<Role>("Receptionist")
        ?? throw new InvalidOperationException("Receptionist role does not exist.");
    role.AddResponsibility(dataContext, "Export contacts");
}

Summary

Step What Where
1 Define responsibilities with entity/property permissions [RegisterLogic] method in a [Logic] class
2 Create roles and assign responsibilities [DataGenerator(DataKind.BlankData)] method
3 Assign roles to users API, UI, or [DataGenerator(DataKind.DemoData)]

<< Back to RBAC

<< Back to home page