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:
- Collects all roles attached to the user
- Collects all responsibilities from those roles
- 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)] |