Back to Blog
AuthorizationRBACSecurityAccess Control

Authorization and RBAC Implementation Patterns

Control access to resources. From role-based access control to attribute-based policies to permission systems.

B
Bootspring Team
Engineering
December 20, 2023
5 min read

Authentication verifies identity; authorization controls access. Here's how to implement flexible, secure authorization in your applications.

RBAC Basics#

1// Define roles and permissions 2const roles = { 3 admin: { 4 permissions: ['*'], // All permissions 5 }, 6 editor: { 7 permissions: [ 8 'posts:read', 9 'posts:create', 10 'posts:update', 11 'posts:delete', 12 'comments:read', 13 'comments:moderate', 14 ], 15 }, 16 author: { 17 permissions: [ 18 'posts:read', 19 'posts:create', 20 'posts:update:own', 21 'comments:read', 22 'comments:create', 23 ], 24 }, 25 viewer: { 26 permissions: ['posts:read', 'comments:read'], 27 }, 28}; 29 30// Check permission 31function hasPermission( 32 userRole: string, 33 permission: string 34): boolean { 35 const role = roles[userRole]; 36 if (!role) return false; 37 38 return ( 39 role.permissions.includes('*') || 40 role.permissions.includes(permission) 41 ); 42}

Database Schema#

1// schema.prisma 2model User { 3 id String @id @default(cuid()) 4 email String @unique 5 roles UserRole[] 6} 7 8model Role { 9 id String @id @default(cuid()) 10 name String @unique 11 permissions Permission[] 12 users UserRole[] 13} 14 15model Permission { 16 id String @id @default(cuid()) 17 name String @unique // e.g., "posts:create" 18 roles Role[] 19} 20 21model UserRole { 22 user User @relation(fields: [userId], references: [id]) 23 userId String 24 role Role @relation(fields: [roleId], references: [id]) 25 roleId String 26 assignedAt DateTime @default(now()) 27 28 @@id([userId, roleId]) 29}

Permission Checking Service#

1class AuthorizationService { 2 private permissionCache: Map<string, Set<string>> = new Map(); 3 4 async getUserPermissions(userId: string): Promise<Set<string>> { 5 // Check cache 6 const cached = this.permissionCache.get(userId); 7 if (cached) return cached; 8 9 // Fetch from database 10 const user = await prisma.user.findUnique({ 11 where: { id: userId }, 12 include: { 13 roles: { 14 include: { 15 role: { 16 include: { 17 permissions: true, 18 }, 19 }, 20 }, 21 }, 22 }, 23 }); 24 25 const permissions = new Set<string>(); 26 27 user?.roles.forEach((userRole) => { 28 userRole.role.permissions.forEach((permission) => { 29 permissions.add(permission.name); 30 }); 31 }); 32 33 // Cache for 5 minutes 34 this.permissionCache.set(userId, permissions); 35 setTimeout(() => this.permissionCache.delete(userId), 5 * 60 * 1000); 36 37 return permissions; 38 } 39 40 async can( 41 userId: string, 42 permission: string, 43 resource?: any 44 ): Promise<boolean> { 45 const permissions = await this.getUserPermissions(userId); 46 47 // Check exact permission 48 if (permissions.has(permission)) return true; 49 50 // Check wildcard 51 if (permissions.has('*')) return true; 52 53 // Check resource-level permission (e.g., posts:update:own) 54 const [resource_type, action] = permission.split(':'); 55 if (permissions.has(`${resource_type}:${action}:own`)) { 56 return resource?.userId === userId; 57 } 58 59 return false; 60 } 61 62 invalidateCache(userId: string) { 63 this.permissionCache.delete(userId); 64 } 65} 66 67const authz = new AuthorizationService();

Express Middleware#

1// Permission middleware 2function requirePermission(permission: string) { 3 return async (req: Request, res: Response, next: NextFunction) => { 4 const userId = req.user?.id; 5 6 if (!userId) { 7 return res.status(401).json({ error: 'Authentication required' }); 8 } 9 10 const allowed = await authz.can(userId, permission); 11 12 if (!allowed) { 13 return res.status(403).json({ error: 'Permission denied' }); 14 } 15 16 next(); 17 }; 18} 19 20// Resource-level permission 21function requireResourcePermission( 22 permission: string, 23 getResource: (req: Request) => Promise<any> 24) { 25 return async (req: Request, res: Response, next: NextFunction) => { 26 const userId = req.user?.id; 27 28 if (!userId) { 29 return res.status(401).json({ error: 'Authentication required' }); 30 } 31 32 const resource = await getResource(req); 33 34 if (!resource) { 35 return res.status(404).json({ error: 'Resource not found' }); 36 } 37 38 const allowed = await authz.can(userId, permission, resource); 39 40 if (!allowed) { 41 return res.status(403).json({ error: 'Permission denied' }); 42 } 43 44 req.resource = resource; 45 next(); 46 }; 47} 48 49// Usage 50app.get('/posts', requirePermission('posts:read'), getPosts); 51 52app.put( 53 '/posts/:id', 54 requireResourcePermission('posts:update', async (req) => { 55 return prisma.post.findUnique({ where: { id: req.params.id } }); 56 }), 57 updatePost 58);

ABAC (Attribute-Based)#

1// More flexible than RBAC 2interface Policy { 3 resource: string; 4 action: string; 5 condition: (context: PolicyContext) => boolean; 6} 7 8interface PolicyContext { 9 user: User; 10 resource: any; 11 environment: { 12 time: Date; 13 ip: string; 14 }; 15} 16 17const policies: Policy[] = [ 18 { 19 resource: 'posts', 20 action: 'update', 21 condition: ({ user, resource }) => { 22 return user.id === resource.authorId || user.role === 'admin'; 23 }, 24 }, 25 { 26 resource: 'reports', 27 action: 'download', 28 condition: ({ user, environment }) => { 29 // Only during business hours 30 const hour = environment.time.getHours(); 31 return user.role === 'analyst' && hour >= 9 && hour <= 17; 32 }, 33 }, 34 { 35 resource: 'admin', 36 action: '*', 37 condition: ({ user, environment }) => { 38 // Admin access only from office IP 39 return user.role === 'admin' && 40 environment.ip.startsWith('10.0.'); 41 }, 42 }, 43]; 44 45class PolicyEngine { 46 evaluate( 47 resource: string, 48 action: string, 49 context: PolicyContext 50 ): boolean { 51 const matchingPolicies = policies.filter( 52 (p) => 53 (p.resource === resource || p.resource === '*') && 54 (p.action === action || p.action === '*') 55 ); 56 57 return matchingPolicies.some((policy) => policy.condition(context)); 58 } 59}

Role Hierarchy#

1// Roles inherit from parent roles 2const roleHierarchy: Record<string, string[]> = { 3 admin: ['editor', 'author', 'viewer'], 4 editor: ['author', 'viewer'], 5 author: ['viewer'], 6 viewer: [], 7}; 8 9function getEffectiveRoles(role: string): string[] { 10 const roles = [role]; 11 const inherited = roleHierarchy[role] || []; 12 13 for (const inheritedRole of inherited) { 14 roles.push(...getEffectiveRoles(inheritedRole)); 15 } 16 17 return [...new Set(roles)]; 18} 19 20// admin -> ['admin', 'editor', 'author', 'viewer']

Frontend Integration#

1// React context for permissions 2const AuthContext = createContext<{ 3 user: User | null; 4 permissions: string[]; 5 can: (permission: string) => boolean; 6}>({ user: null, permissions: [], can: () => false }); 7 8function AuthProvider({ children }: { children: React.ReactNode }) { 9 const [user, setUser] = useState<User | null>(null); 10 const [permissions, setPermissions] = useState<string[]>([]); 11 12 useEffect(() => { 13 fetchCurrentUser().then(({ user, permissions }) => { 14 setUser(user); 15 setPermissions(permissions); 16 }); 17 }, []); 18 19 const can = (permission: string) => { 20 return permissions.includes(permission) || permissions.includes('*'); 21 }; 22 23 return ( 24 <AuthContext.Provider value={{ user, permissions, can }}> 25 {children} 26 </AuthContext.Provider> 27 ); 28} 29 30// Permission component 31function Can({ 32 permission, 33 children, 34 fallback = null, 35}: { 36 permission: string; 37 children: React.ReactNode; 38 fallback?: React.ReactNode; 39}) { 40 const { can } = useContext(AuthContext); 41 return can(permission) ? <>{children}</> : <>{fallback}</>; 42} 43 44// Usage 45<Can permission="posts:create"> 46 <Button>Create Post</Button> 47</Can> 48 49<Can permission="admin:access" fallback={<AccessDenied />}> 50 <AdminPanel /> 51</Can>

Best Practices#

DO: ✓ Deny by default ✓ Check permissions on both frontend and backend ✓ Cache permissions appropriately ✓ Log access attempts ✓ Use principle of least privilege ✓ Audit permission changes DON'T: ✗ Trust client-side permission checks alone ✗ Hardcode permissions in code ✗ Give admin access by default ✗ Forget to invalidate cache on changes

Conclusion#

Start with simple RBAC, evolve to ABAC if needed. Always enforce authorization on the server, cache permissions for performance, and audit access for security.

The best authorization system is one that's strict by default and flexible when needed.

Share this article

Help spread the word about Bootspring