Recommended patterns and anti-patterns for using RBAC effectively.
- Permission Design
- Role Design
- Performance Optimization
- Security Considerations
- Testing Strategies
- Common Patterns
- Anti-Patterns
Good:
// Resource:action pattern
'user:read'
'user:write'
'user:delete'
'post:read'
'post:write'
'post:delete'Bad:
// Inconsistent patterns
'readUser'
'user_write'
'deletePost'
'post.read'Good:
'post:read'
'post:write'
'post:delete'
'post:publish'Bad:
'post:manage' // Too broad, unclear what it includesGood:
'admin:users:read'
'admin:users:write'
'admin:users:delete'
'admin:settings:read'
'admin:settings:write'Bad:
'admin_users_read'
'usersRead'Bad:
// 100+ individual permissions
'post:read'
'post:read:own'
'post:read:others'
'post:read:published'
'post:read:draft'
// ... 95 moreGood:
// Use context-based checks instead
'post:read' // Check ownership in business logic
function canReadPost(user: User, post: Post): boolean {
if (rbac.hasPermission(user, 'post:read:all')) return true;
if (rbac.hasPermission(user, 'post:read:own') && post.authorId === user.id) return true;
return false;
}// Good - with documentation
const PERMISSIONS = {
// User management
USER_READ: 'user:read', // View user profiles and lists
USER_WRITE: 'user:write', // Create and update users
USER_DELETE: 'user:delete', // Permanently delete users
// Post management
POST_READ: 'post:read', // View published posts
POST_WRITE: 'post:write', // Create and edit own posts
POST_PUBLISH: 'post:publish', // Publish posts (make public)
POST_DELETE: 'post:delete', // Delete any post
} as const;Good:
rbac.createRole('content-editor', [
'post:read',
'post:write',
'post:publish'
]);
rbac.createRole('content-moderator', [
'post:read',
'post:flag',
'comment:moderate'
]);Bad:
// Roles based on individuals
rbac.createRole('johns-role', [...]);
rbac.createRole('marys-role', [...]);Good:
// Base roles
rbac.createRole('reader', ['post:read', 'comment:read']);
rbac.createRole('writer', ['post:read', 'post:write']);
rbac.createRole('publisher', ['post:read', 'post:write', 'post:publish']);
// User can have multiple roles
const user = {
id: '1',
roles: ['writer', 'moderator'] // Combines permissions from both
};Bad:
// Monolithic roles with duplicated permissions
rbac.createRole('writer', ['post:read', 'post:write']);
rbac.createRole('writer-moderator', ['post:read', 'post:write', 'comment:moderate']);
rbac.createRole('writer-publisher', ['post:read', 'post:write', 'post:publish']);Good:
hierarchy.setRoleLevel('user', 1);
hierarchy.setRoleLevel('moderator', 5);
hierarchy.setRoleLevel('admin', 10);
hierarchy.setRoleLevel('super-admin', 100);
// Clear hierarchy with gaps for future rolesBad:
hierarchy.setRoleLevel('user', 1);
hierarchy.setRoleLevel('moderator', 2);
hierarchy.setRoleLevel('admin', 3);
// No room to add intermediate rolesBad:
// 50+ hyper-specific roles
'junior-content-editor'
'senior-content-editor'
'junior-content-editor-with-publish'
'senior-content-editor-without-delete'
// ...Good:
// Fewer roles + direct permissions for exceptions
rbac.createRole('content-editor', [...]);
rbac.createRole('senior-editor', [...]);
// Use direct permissions for exceptions
seniorEditor.permissions = ['post:publish'];Good:
const rbac = new RBAC({ useBitSystem: true });
// O(1) permission checks
rbac.hasPermission(user, 'post:read');Only use string-based if:
- You have > 31 permissions
- You're in development/prototyping
Good:
// Calculate mask once
const editorMask = rbac.getRoleMask('editor');
// Reuse for many users
users.forEach(user => {
user.permissionMask = editorMask;
});Bad:
// Recalculating for each user
users.forEach(user => {
user.roles = ['editor'];
// Forces recalculation on every check
});Good:
const logger = new BufferedAuditLogger(
async (events) => {
await database.auditLogs.insertMany(events);
},
{
maxBufferSize: 100,
flushIntervalMs: 5000
}
);
const rbac = new RBAC({ auditLogger: logger });Bad:
// Synchronous logger that blocks on every check
const logger = {
log: (event) => {
database.auditLogs.insertOne(event); // Blocks permission check!
}
};Good:
// For expensive context-based checks
const permissionCache = new Map<string, boolean>();
function canEditPost(user: User, post: Post): boolean {
const cacheKey = `${user.id}:${post.id}:edit`;
if (permissionCache.has(cacheKey)) {
return permissionCache.get(cacheKey)!;
}
const canEdit = rbac.hasPermission(user, 'post:edit') &&
(post.authorId === user.id || rbac.hasPermission(user, 'post:edit:all'));
permissionCache.set(cacheKey, canEdit);
return canEdit;
}Good:
function checkPermission(userId: string, permission: string): boolean {
// Validate inputs
if (!userId || typeof userId !== 'string') {
throw new Error('Invalid user ID');
}
if (!permission || typeof permission !== 'string') {
throw new Error('Invalid permission');
}
// Sanitize permission string
if (!/^[a-z0-9:*]+$/i.test(permission)) {
throw new Error('Invalid permission format');
}
return rbac.hasPermission(user, permission);
}Good:
// Suspend suspicious user immediately
rbac.denyPermission(suspiciousUserId, '*');
// Block specific dangerous operations
rbac.denyPermission(userId, 'admin:delete:*');Good:
const rbac = new RBAC({
auditLogger: new BufferedAuditLogger(
async (events) => {
// Log all permission checks for security audit
await securityLog.insertMany(events);
// Alert on suspicious patterns
const failedAttempts = events.filter(e => !e.allowed);
if (failedAttempts.length > 10) {
await alertSecurity(failedAttempts);
}
}
)
});Bad:
// Client-side (React)
if (rbac.hasPermission(user, 'post:delete')) {
// Show delete button
deletePost(postId); // DANGER: Client can bypass this!
}Good:
// Client-side (React)
if (rbac.hasPermission(user, 'post:delete')) {
// Show delete button (UI only)
}
// Server-side (required!)
app.delete('/posts/:id', (req, res) => {
if (!rbac.hasPermission(req.user, 'post:delete')) {
return res.status(403).json({ error: 'Forbidden' });
}
deletePost(req.params.id);
});Good:
const rbac = new RBAC({ strictMode: true });
// Throws error on invalid operations
rbac.hasPermission(null, 'post:read'); // Throws!Bad:
// API endpoint
app.get('/rbac/state', (req, res) => {
res.json(rbac.serialize()); // DANGER: Exposes all permissions!
});Good:
// Only send user's own permissions
app.get('/me/permissions', (req, res) => {
const user = req.user;
const permissions = rbac.getUserPermissions(user);
res.json({ permissions });
});Good:
describe('Post permissions', () => {
it('should allow editors to write posts', () => {
const editor = { id: '1', roles: ['editor'] };
expect(rbac.hasPermission(editor, 'post:write')).toBe(true);
});
it('should deny readers from writing posts', () => {
const reader = { id: '2', roles: ['reader'] };
expect(rbac.hasPermission(reader, 'post:write')).toBe(false);
});
it('should respect deny permissions', () => {
const editor = { id: '1', roles: ['editor'] };
rbac.denyPermission('1', 'post:write');
expect(rbac.hasPermission(editor, 'post:write')).toBe(false);
});
});Good:
describe('Role hierarchy', () => {
it('should allow admin to act as editor', () => {
expect(rbac.canActAsRole('admin', 'editor')).toBe(true);
});
it('should prevent editor from acting as admin', () => {
expect(rbac.canActAsRole('editor', 'admin')).toBe(false);
});
});Good:
describe('Wildcard permissions', () => {
it('should match admin:* pattern', () => {
const admin = { id: '1', roles: ['admin'] };
expect(rbac.hasPermission(admin, 'admin:users')).toBe(true);
expect(rbac.hasPermission(admin, 'admin:settings')).toBe(true);
});
it('should not match non-admin permissions', () => {
const admin = { id: '1', roles: ['admin'] };
expect(rbac.hasPermission(admin, 'user:read')).toBe(false);
});
});Good:
describe('Audit logging', () => {
it('should log permission checks', () => {
const events: AuditEvent[] = [];
const logger = { log: (event: AuditEvent) => events.push(event) };
const rbac = new RBAC({ auditLogger: logger });
rbac.hasPermission(user, 'post:read');
expect(events).toHaveLength(1);
expect(events[0].permission).toBe('post:read');
});
});// Tenant isolation with wildcards
rbac.createRole('tenant-owner', ['tenant:*']);
rbac.createRole('tenant-admin', ['tenant:users:*', 'tenant:data:*']);
rbac.createRole('tenant-user', ['tenant:data:read']);
// Check with tenant context
function hasPermissionInTenant(user: User, permission: string, tenantId: string): boolean {
const tenantPermission = `tenant:${tenantId}:${permission}`;
return rbac.hasPermission(user, tenantPermission);
}// Combine RBAC with resource ownership
function canEditPost(user: User, post: Post): boolean {
// Can edit if has global permission
if (rbac.hasPermission(user, 'post:edit:all')) return true;
// Can edit own posts if has permission
if (rbac.hasPermission(user, 'post:edit:own') && post.authorId === user.id) {
return true;
}
return false;
}// Grant temporary access
function grantTemporaryAccess(userId: string, permission: string, durationMs: number) {
const user = getUser(userId);
// Add direct permission
if (!user.permissions) user.permissions = [];
user.permissions.push(permission);
// Remove after duration
setTimeout(() => {
user.permissions = user.permissions.filter(p => p !== permission);
}, durationMs);
}
// Usage
grantTemporaryAccess('user-1', 'admin:view', 3600000); // 1 hour// Use permissions as feature flags
const FEATURES = {
NEW_EDITOR: 'feature:new-editor',
BETA_API: 'feature:beta-api',
DARK_MODE: 'feature:dark-mode',
};
// Grant to specific users
rbac.denyPermission('user-1', FEATURES.NEW_EDITOR); // Opt out
user.permissions = [FEATURES.BETA_API]; // Opt in
// Check in code
if (rbac.hasPermission(user, FEATURES.NEW_EDITOR)) {
return <NewEditor />;
}// Use audit logs for rate limiting
class RateLimitLogger implements AuditLogger {
private attempts = new Map<string, number[]>();
log(event: AuditEvent): void {
const key = event.userId;
const now = Date.now();
if (!this.attempts.has(key)) {
this.attempts.set(key, []);
}
const userAttempts = this.attempts.get(key)!;
userAttempts.push(now);
// Keep only last minute
this.attempts.set(key, userAttempts.filter(t => now - t < 60000));
// Block if too many attempts
if (userAttempts.length > 100) {
rbac.denyPermission(key, '*');
}
}
}Bad:
'user:read:if:admin'
'post:write:only:owner'Why: Permissions should be simple identifiers. Use business logic for complex rules.
Good:
'user:read'
'post:write'
// Complex logic in code
function canWritePost(user: User, post: Post): boolean {
if (rbac.hasPermission(user, 'post:write:all')) return true;
if (rbac.hasPermission(user, 'post:write') && post.authorId === user.id) return true;
return false;
}Bad:
'user:status:active'
'user:status:suspended'
'user:status:deleted'Why: Status is data, not permission. Use user properties.
Good:
user.status = 'active' | 'suspended' | 'deleted';
if (user.status === 'suspended') {
rbac.denyPermission(user.id, '*');
}Bad:
`user:${userId}:read`
`post:${postId}:write`Why: Creates infinite permissions. Use context instead.
Good:
'user:read'
'post:write'
function canAccessResource(user: User, resourceId: string): boolean {
const resource = getResource(resourceId);
return rbac.hasPermission(user, 'resource:read') &&
(resource.ownerId === user.id || rbac.hasPermission(user, 'resource:read:all'));
}Bad:
// Every user has 20+ direct permissions
user.permissions = [
'post:read',
'post:write',
'comment:read',
'comment:write',
// ... 15 more
];Why: Hard to manage, defeats purpose of roles.
Good:
// Use roles for common permission sets
rbac.createRole('content-creator', [
'post:read',
'post:write',
'comment:read',
'comment:write'
]);
user.roles = ['content-creator'];
// Use direct permissions only for exceptions
user.permissions = ['beta:new-feature'];Bad:
// Admins need to be assigned every permission
rbac.createRole('admin', [
'user:read',
'user:write',
'user:delete',
'post:read',
'post:write',
'post:delete',
// ... 100 more permissions
]);Good:
// Use wildcards and hierarchy
rbac.createRole('admin', ['*']);
hierarchy.setRoleLevel('admin', 10);- All permissions follow consistent naming convention
- Roles are based on job functions, not individuals
- Using bit-based system (if ≤ 31 permissions)
- Audit logging enabled with buffered logger
- Server-side permission checks on all protected endpoints
- Strict mode enabled
- Comprehensive tests for all permission scenarios
- Role hierarchy configured appropriately
- Permission documentation created
- Security audit completed
See also:
- Core Concepts - Understanding RBAC fundamentals
- API Reference - Complete API documentation
- Performance Guide - Optimization tips