Role-Based Access Control (RBAC)
Role-Based Access Control (RBAC)
Section titled “Role-Based Access Control (RBAC)”This guide covers implementing Role-Based Access Control (RBAC) to manage user permissions through roles rather than direct permission assignments.
Use This Guide If…
Section titled “Use This Guide If…”- You’re managing permissions for multiple users
- You want to group permissions into reusable roles
- You need to simplify permission management
- You’re implementing hierarchical access control
- You want to understand role assignment patterns
RBAC Overview
Section titled “RBAC Overview”Role-Based Access Control (RBAC) simplifies permission management by:
- Grouping Permissions: Bundle related permissions into roles
- Assigning Roles to Users: Users inherit all permissions from assigned roles
- Centralized Management: Update role permissions to affect all users with that role
- Scalability: Easier to manage than individual user permissions
RBAC Benefits
Section titled “RBAC Benefits”- ✓ Simplified Management: Change one role instead of many users
- ✓ Consistency: All users in a role have same permissions
- ✓ Auditability: Clear visibility into who has what access
- ✓ Scalability: Grows with organization without complexity
- ✓ Compliance: Easier to demonstrate access control policies
RBAC Components
Section titled “RBAC Components”A role represents a job function or responsibility:
interface Role { id: string; name: string; description: string; permissions: string[]; // All permissions granted by this role isActive: boolean; createdAt: Date; createdBy: string;}Example roles:
// Admin role - full access{ id: 'role-admin', name: 'admin', description: 'Full system access', permissions: [ 'user:create', 'user:read', 'user:update', 'user:delete', 'product:create', 'product:read', 'product:update', 'product:delete', 'order:create', 'order:read', 'order:update', 'order:delete', 'reports:view', 'reports:export', 'reports:schedule', ], isActive: true}
// Manager role - can manage products and view reports{ id: 'role-manager', name: 'manager', description: 'Product management and reporting', permissions: [ 'product:create', 'product:read', 'product:update', 'order:read', 'reports:view', 'reports:export', ], isActive: true}
// User role - basic access{ id: 'role-user', name: 'user', description: 'Standard user access', permissions: [ 'product:read', 'order:create', 'order:read', ], isActive: true}User-Role Assignment
Section titled “User-Role Assignment”Users can have multiple roles:
interface User { id: string; email: string; roles: string[]; // Array of role IDs directPermissions: string[]; // Optional: Direct permissions beyond roles}Permission Resolution
Section titled “Permission Resolution”User’s total permissions = Role permissions + Direct permissions (deduplicated):
async function getUserPermissions(user: User): Promise<string[]> { // 1. Get permissions from all assigned roles const rolePermissions: string[] = []; for (const roleId of user.roles) { const role = await RoleRepository.findById(roleId); if (role?.isActive) { rolePermissions.push(...role.permissions); } }
// 2. Add direct permissions const directPermissions = user.directPermissions || [];
// 3. Combine and deduplicate return Array.from(new Set([...rolePermissions, ...directPermissions]));}Creating Roles
Section titled “Creating Roles”Using Auth Service Commands
Section titled “Using Auth Service Commands”// Create a roleimport { authServiceClient } from './clients.js';
const result = await authServiceClient.createRole({ name: 'manager', description: 'Product management and reporting', permissions: [ 'product:create', 'product:read', 'product:update', 'order:read', 'reports:view', 'reports:export', ],});
// Result:// {// roleId: 'role-uuid',// name: 'manager',// permissions: ['product:create', 'product:read', ...]// }Direct Event Sourcing
Section titled “Direct Event Sourcing”import { CommandHandler, CommandHandlerDecorator } from '@banyanai/platform-base-service';import { RoleAggregate } from '../aggregates/RoleAggregate.js';
@CommandHandlerDecorator(CreateRoleCommand)export class CreateRoleHandler extends CommandHandler<CreateRoleCommand, CreateRoleResult> { async handle(command: CreateRoleCommand, user: AuthenticatedUser | null): Promise<CreateRoleResult> { if (!user) { throw new Error('Authentication required to create roles'); }
// Create role aggregate const roleId = uuidv4(); const role = new RoleAggregate(roleId);
// Execute domain logic role.createRole( roleId, command.name, command.description, command.permissions, user.userId );
// Save to event store (automatic) await this.save(role);
return { roleId, name: command.name, permissions: command.permissions, }; }}Assigning Roles to Users
Section titled “Assigning Roles to Users”Assign Role Command
Section titled “Assign Role Command”// Assign role to userconst result = await authServiceClient.assignRoleToUser({ userId: 'user-123', roleId: 'role-manager',});
// User now inherits all permissions from 'manager' roleImplementation
Section titled “Implementation”export class UserAggregate extends AggregateRoot { private roles: string[] = [];
assignRole(roleId: string, assignedBy: string): void { if (this.roles.includes(roleId)) { throw new Error('User already has this role'); }
this.raiseEvent( { userId: this.id, roleId, assignedAt: new Date(), assignedBy, }, 'RoleAssignedToUser' ); }
removeRole(roleId: string, removedBy: string): void { if (!this.roles.includes(roleId)) { throw new Error('User does not have this role'); }
this.raiseEvent( { userId: this.id, roleId, removedAt: new Date(), removedBy, }, 'RoleRemovedFromUser' ); }
applyRoleAssignedToUser(event: RoleAssignedToUserEvent): void { this.roles.push(event.roleId); }
applyRoleRemovedFromUser(event: RoleRemovedFromUserEvent): void { this.roles = this.roles.filter(r => r !== event.roleId); }}Permission Resolution in JWT
Section titled “Permission Resolution in JWT”When generating JWT tokens, resolve all permissions from roles:
async handle(command: AuthenticateUserCommand) { // Validate credentials const user = await this.validateCredentials(command.email, command.password);
// Resolve permissions from roles + direct permissions const permissions = await this.getUserPermissions(user);
// Get role names for JWT const roles = await this.getUserRoles(user);
// Generate JWT with both roles and resolved permissions const tokens = await this.jwtManager.generateTokenPair({ userId: user.id, email: user.email, permissions, // Flattened permissions for Layer 1 checks roles: roles.map(r => r.name), // Role names for Layer 2 policies });
return { success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, };}
private async getUserPermissions(user: UserReadModel): Promise<string[]> { // Direct permissions const directPermissions = user.directPermissions || [];
// Role-based permissions const rolePermissions: string[] = []; for (const roleId of user.roles || []) { const role = await RoleReadModel.findById(roleId); if (role?.isActive && role.permissions) { rolePermissions.push(...role.permissions); } }
// Combine and deduplicate return Array.from(new Set([...directPermissions, ...rolePermissions]));}JWT Token with Roles
Section titled “JWT Token with Roles”{ "sub": "user-123", "email": "alice@example.com", "name": "Alice Smith", "permissions": [ "product:create", "product:read", "product:update", "order:read", "reports:view", "reports:export" ], "roles": ["manager"], "iat": 1705334400, "exp": 1705334700}Using Roles in Authorization
Section titled “Using Roles in Authorization”Layer 1: Permission-Based (Automatic)
Section titled “Layer 1: Permission-Based (Automatic)”Layer 1 uses resolved permissions - roles are transparent:
// Contract requires permission@Command({ permissions: ['product:create']})export class CreateProductCommand { ... }
// User with 'manager' role has this permission// Gateway checks user.permissions array (which includes role permissions)// Authorization passes ✓Layer 2: Role-Based Policies
Section titled “Layer 2: Role-Based Policies”Layer 2 can check roles directly for business logic:
import { RequirePolicy } from '@banyanai/platform-base-service';import { PolicyViolationError } from '@banyanai/platform-core';
@RequirePolicy(async (user: AuthenticatedUser | null, command: DeleteUserCommand) => { if (!user) { throw new PolicyAuthenticationError('delete_user'); }
// Check if user has admin role if (!user.roles?.includes('admin')) { throw new PolicyViolationError( 'DeleteUserHandler', user.userId, 'delete_user', 'Only administrators can delete users' ); }
// Can't delete yourself if (command.userId === user.userId) { throw new PolicyViolationError( 'DeleteUserHandler', user.userId, 'delete_user', 'Cannot delete your own account' ); }})Common Role Patterns
Section titled “Common Role Patterns”Pattern 1: Standard Hierarchy
Section titled “Pattern 1: Standard Hierarchy”// Super Admin - can do anythingconst superAdminRole = { name: 'super-admin', permissions: ['*:*'], // If wildcards supported // OR list all permissions explicitly};
// Admin - full CRUD on all resourcesconst adminRole = { name: 'admin', permissions: [ 'user:create', 'user:read', 'user:update', 'user:delete', 'product:create', 'product:read', 'product:update', 'product:delete', 'order:create', 'order:read', 'order:update', 'order:delete', 'reports:view', 'reports:export', 'reports:schedule', ],};
// Manager - manage specific domainsconst managerRole = { name: 'manager', permissions: [ 'product:create', 'product:read', 'product:update', 'order:read', 'order:update', 'reports:view', 'reports:export', ],};
// User - read and create own dataconst userRole = { name: 'user', permissions: [ 'product:read', 'order:create', 'order:read', ],};Pattern 2: Department-Based Roles
Section titled “Pattern 2: Department-Based Roles”// Sales departmentconst salesRole = { name: 'sales', permissions: [ 'order:create', 'order:read', 'order:update', 'customer:create', 'customer:read', 'customer:update', 'reports:view', ],};
// Warehouse departmentconst warehouseRole = { name: 'warehouse', permissions: [ 'product:read', 'product:update', 'inventory:create', 'inventory:read', 'inventory:update', 'shipment:create', 'shipment:read', 'shipment:update', ],};
// Finance departmentconst financeRole = { name: 'finance', permissions: [ 'order:read', 'invoice:create', 'invoice:read', 'invoice:update', 'payment:create', 'payment:read', 'reports:view', 'reports:export', 'reports:schedule', ],};Pattern 3: Feature-Based Roles
Section titled “Pattern 3: Feature-Based Roles”// Analytics viewerconst analyticsViewerRole = { name: 'analytics-viewer', permissions: [ 'reports:view', 'analytics:view', 'dashboard:view', ],};
// Report exporterconst reportExporterRole = { name: 'report-exporter', permissions: [ 'reports:view', 'reports:export', 'analytics:view', 'analytics:export', ],};Pattern 4: Multiple Roles per User
Section titled “Pattern 4: Multiple Roles per User”Users can have multiple roles for combined permissions:
// User is both a manager and analytics viewerconst user = { id: 'user-123', email: 'alice@example.com', roles: ['manager', 'analytics-viewer'], // Inherits permissions from both roles: // - manager: product:*, order:*, reports:view, reports:export // - analytics-viewer: analytics:view, dashboard:view};
// Total permissions = union of all role permissionsUpdating Roles
Section titled “Updating Roles”Update Role Permissions
Section titled “Update Role Permissions”// Update role permissionsconst result = await authServiceClient.updateRolePermissions({ roleId: 'role-manager', permissions: [ // Add new permission 'product:delete', // Keep existing 'product:create', 'product:read', 'product:update', 'order:read', 'reports:view', 'reports:export', ],});
// All users with 'manager' role now have 'product:delete' permission// (on their next token refresh)⚠️ Note: Permission changes only affect users after they get new access tokens (next login or token refresh).
Permission Refresh Strategy
Section titled “Permission Refresh Strategy”// Option 1: Users refresh tokens periodically// Access tokens expire after 5 minutes, users get new token with current permissions
// Option 2: Force token refresh for all users in roleasync function forceRoleTokenRefresh(roleId: string) { // 1. Get all users with this role const users = await UserReadModel.findByRole(roleId);
// 2. Invalidate their refresh tokens for (const user of users) { await RefreshTokenRepository.revokeAllForUser(user.id); }
// 3. Users must re-authenticate to get new tokens // New tokens will include updated permissions}
// Option 3: Real-time permission updates (advanced)// Publish permission change event via WebSocket/SSE// Client fetches new token immediatelyQuerying Roles and Permissions
Section titled “Querying Roles and Permissions”Get User’s Roles
Section titled “Get User’s Roles”// Query user's assigned rolesconst result = await authServiceClient.getUserRoles({ userId: 'user-123',});
// Returns:// {// roles: [// { id: 'role-manager', name: 'manager' },// { id: 'role-analytics', name: 'analytics-viewer' }// ]// }Get User’s Effective Permissions
Section titled “Get User’s Effective Permissions”// Query user's total permissions (roles + direct)const result = await authServiceClient.getUserPermissions({ userId: 'user-123',});
// Returns:// {// permissions: [// 'product:create', 'product:read', 'product:update',// 'order:read',// 'reports:view', 'reports:export',// 'analytics:view', 'dashboard:view'// ],// sources: {// 'product:create': ['role:manager'],// 'product:read': ['role:manager'],// 'analytics:view': ['role:analytics-viewer'],// // ... permission source tracking// }// }List All Roles
Section titled “List All Roles”// Query all available rolesconst result = await authServiceClient.listRoles({});
// Returns:// {// roles: [// {// id: 'role-admin',// name: 'admin',// description: 'Full system access',// permissionCount: 20,// userCount: 2,// },// {// id: 'role-manager',// name: 'manager',// description: 'Product management',// permissionCount: 8,// userCount: 15,// },// // ...// ]// }Testing RBAC
Section titled “Testing RBAC”Unit Testing Role Creation
Section titled “Unit Testing Role Creation”import { describe, expect, test } from '@jest/globals';import { RoleAggregate } from '../aggregates/RoleAggregate.js';
describe('RoleAggregate', () => { test('should create role with valid permissions', () => { const role = new RoleAggregate('role-123');
role.createRole( 'role-123', 'manager', 'Product manager role', ['product:create', 'product:read', 'product:update'], 'admin-user' );
const events = role.getUncommittedEvents(); expect(events).toHaveLength(1); expect(events[0].type).toBe('RoleCreated'); expect(events[0].data.name).toBe('manager'); expect(events[0].data.permissions).toContain('product:create'); });
test('should reject invalid permission format', () => { const role = new RoleAggregate('role-123');
expect(() => { role.createRole( 'role-123', 'manager', 'Manager', ['invalid-permission'], // Missing ':' separator 'admin-user' ); }).toThrow('Invalid permission format'); });});Integration Testing Permission Resolution
Section titled “Integration Testing Permission Resolution”describe('Permission Resolution', () => { test('should combine permissions from multiple roles', async () => { // Create user with multiple roles const user = await createTestUser({ roles: ['manager', 'analytics-viewer'], });
// Get effective permissions const permissions = await authService.getUserPermissions(user.id);
// Should have permissions from both roles expect(permissions).toContain('product:create'); // From manager expect(permissions).toContain('analytics:view'); // From analytics-viewer });
test('should deduplicate permissions from overlapping roles', async () => { // Both roles have 'product:read' const user = await createTestUser({ roles: ['manager', 'user'], });
const permissions = await authService.getUserPermissions(user.id);
// Permission should appear only once const readPermissions = permissions.filter(p => p === 'product:read'); expect(readPermissions).toHaveLength(1); });});Security Best Practices
Section titled “Security Best Practices”1. Principle of Least Privilege
Section titled “1. Principle of Least Privilege”// ✓ GOOD: Role has only necessary permissionsconst salesRole = { name: 'sales', permissions: [ 'order:create', 'order:read', 'customer:read', ],};
// ✗ BAD: Role has excessive permissionsconst salesRole = { name: 'sales', permissions: [ 'order:*', // Too broad 'customer:*', // Can delete customers! 'product:*', // Not needed for sales ],};2. Separate Admin Roles
Section titled “2. Separate Admin Roles”// ✓ GOOD: Specific admin rolesconst userAdminRole = { name: 'user-admin', permissions: ['user:create', 'user:read', 'user:update', 'user:delete'],};
const productAdminRole = { name: 'product-admin', permissions: ['product:create', 'product:read', 'product:update', 'product:delete'],};
// ✗ BAD: Single god-mode adminconst adminRole = { name: 'admin', permissions: ['*:*'], // Too powerful, no separation};3. Audit Role Changes
Section titled “3. Audit Role Changes”// Log all role modificationsLogger.info('Role updated', { roleId: role.id, roleName: role.name, permissionsAdded: ['product:delete'], permissionsRemoved: [], updatedBy: user.userId, affectedUserCount: 15,});
// Log role assignmentsLogger.info('Role assigned to user', { userId: user.id, roleId: role.id, roleName: role.name, assignedBy: admin.userId, permissionsGranted: role.permissions,});4. Role Naming Conventions
Section titled “4. Role Naming Conventions”// ✓ GOOD: Clear, descriptive names'user-admin''product-manager''sales-representative''analytics-viewer'
// ✗ BAD: Vague names'role1''admin''power-user'5. Regular Permission Audits
Section titled “5. Regular Permission Audits”// Periodic audit: Find users with admin permissionsasync function auditAdminAccess() { const users = await UserReadModel.findAll();
for (const user of users) { const permissions = await getUserPermissions(user);
const hasAdminPerms = permissions.some(p => p.includes(':delete') || p.includes('admin') );
if (hasAdminPerms) { Logger.info('Admin access detected', { userId: user.id, email: user.email, roles: user.roles, adminPermissions: permissions.filter(p => p.includes(':delete') || p.includes('admin') ), }); } }}Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”❌ Mistake 1: Hardcoding Role Checks
Section titled “❌ Mistake 1: Hardcoding Role Checks”// BAD: Hardcoded role checkif (user.roles.includes('admin')) { // allow}
// GOOD: Use permissions, not roles// Let RBAC handle permission resolution@Command({ permissions: ['user:delete'] })❌ Mistake 2: Not Deduplicating Permissions
Section titled “❌ Mistake 2: Not Deduplicating Permissions”// BAD: User ends up with duplicate permissionspermissions: ['product:read', 'product:read', 'order:read']
// GOOD: Deduplicate when resolvingpermissions: Array.from(new Set([...rolePerms, ...directPerms]))❌ Mistake 3: Forgetting Inactive Roles
Section titled “❌ Mistake 3: Forgetting Inactive Roles”// BAD: Including inactive rolesfor (const roleId of user.roles) { const role = await RoleReadModel.findById(roleId); rolePermissions.push(...role.permissions); // What if role.isActive === false?}
// GOOD: Filter inactive rolesfor (const roleId of user.roles) { const role = await RoleReadModel.findById(roleId); if (role?.isActive) { rolePermissions.push(...role.permissions); }}❌ Mistake 4: No Permission Refresh Strategy
Section titled “❌ Mistake 4: No Permission Refresh Strategy”// BAD: Users keep old permissions forever// Update role permissions but users never get updated tokens
// GOOD: Force token refresh or use short token expiry// Access tokens expire after 5 minutes// Users automatically get refreshed permissionsNext Steps
Section titled “Next Steps”Now that you understand RBAC:
- External Auth: Learn external auth providers integration
- Policies: Review policy-based authorization for business rules
- Testing: Read testing services
Related Guides
Section titled “Related Guides”- Security Overview - Two-layer authorization model
- Permission-Based Authorization - Layer 1 permissions
- Policy-Based Authorization - Layer 2 policies
- Authentication - JWT tokens and validation
- External Auth Providers - SSO integration