Role based Access Control
Implementing role-based access control (RBAC) in your application to manage user permissions and access.
Overview
Role-based Access Control (RBAC) is a method of regulating access to resources based on the roles of individual users within an organization. RBAC ensures that users can only access the features and data they are authorized to use.
Core RBAC Concepts
Roles
Roles represent a collection of permissions assigned to users.
const roles = {
ADMIN: 'admin',
MANAGER: 'manager',
USER: 'user',
GUEST: 'guest',
};
const roleHierarchy = {
admin: ['admin', 'manager', 'user', 'guest'],
manager: ['manager', 'user', 'guest'],
user: ['user', 'guest'],
guest: ['guest'],
};
Permissions
Permissions define what actions can be performed on resources.
const permissions = {
// User management
USER_CREATE: 'user:create',
USER_READ: 'user:read',
USER_UPDATE: 'user:update',
USER_DELETE: 'user:delete',
// Content management
CONTENT_CREATE: 'content:create',
CONTENT_READ: 'content:read',
CONTENT_UPDATE: 'content:update',
CONTENT_DELETE: 'content:delete',
// Settings
SETTINGS_VIEW: 'settings:view',
SETTINGS_EDIT: 'settings:edit',
};
Role-Permission Mapping
Map permissions to roles.
const rolePermissions = {
admin: [
'user:create',
'user:read',
'user:update',
'user:delete',
'content:create',
'content:read',
'content:update',
'content:delete',
'settings:view',
'settings:edit',
],
manager: [
'user:read',
'user:update',
'content:create',
'content:read',
'content:update',
'content:delete',
'settings:view',
],
user: [
'user:read',
'content:read',
'content:create',
'content:update',
],
guest: [
'content:read',
],
};
Implementation
User Context with RBAC
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const login = async (credentials) => {
// Authenticate user
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
const userData = await response.json();
setUser({
id: userData.id,
name: userData.name,
email: userData.email,
role: userData.role,
permissions: userData.permissions,
});
};
const logout = () => {
setUser(null);
localStorage.removeItem('authToken');
};
const hasPermission = (permission) => {
if (!user) return false;
return user.permissions.includes(permission);
};
const hasRole = (role) => {
if (!user) return false;
return user.role === role;
};
const hasAnyRole = (roles) => {
if (!user) return false;
return roles.includes(user.role);
};
return (
<AuthContext.Provider
value={{
user,
login,
logout,
hasPermission,
hasRole,
hasAnyRole,
}}
>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
Permission Checking Hook
import { useAuth } from './AuthContext';
export const usePermission = (permission) => {
const { hasPermission } = useAuth();
return hasPermission(permission);
};
export const useRole = (role) => {
const { hasRole } = useAuth();
return hasRole(role);
};
// Usage
const DeleteButton = () => {
const canDelete = usePermission('user:delete');
if (!canDelete) return null;
return <button onClick={handleDelete}>Delete User</button>;
};
Component-Level Access Control
Protected Component
const ProtectedComponent = ({ permission, children, fallback = null }) => {
const { hasPermission } = useAuth();
if (!hasPermission(permission)) {
return fallback;
}
return children;
};
// Usage
<ProtectedComponent permission="content:create">
<CreateContentButton />
</ProtectedComponent>
<ProtectedComponent
permission="user:delete"
fallback={<div>You don't have permission to delete users</div>}
>
<DeleteUserButton />
</ProtectedComponent>
Role-Based Component
const RoleBasedComponent = ({ roles, children, fallback = null }) => {
const { hasAnyRole } = useAuth();
if (!hasAnyRole(roles)) {
return fallback;
}
return children;
};
// Usage
<RoleBasedComponent roles={['admin', 'manager']}>
<AdminPanel />
</RoleBasedComponent>
Conditional Rendering Based on Permissions
const UserManagement = () => {
const { hasPermission } = useAuth();
return (
<div>
<h1>User Management</h1>
{hasPermission('user:read') && <UserList />}
{hasPermission('user:create') && (
<button onClick={handleCreateUser}>Create User</button>
)}
{hasPermission('user:update') && (
<button onClick={handleEditUser}>Edit User</button>
)}
{hasPermission('user:delete') && (
<button onClick={handleDeleteUser}>Delete User</button>
)}
</div>
);
};
Route Protection
Protected Route Component
import { Navigate } from 'react-router-dom';
import { useAuth } from './AuthContext';
const ProtectedRoute = ({ permission, children }) => {
const { user, hasPermission } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (permission && !hasPermission(permission)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
// Usage with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/unauthorized" element={<Unauthorized />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin"
element={
<ProtectedRoute permission="settings:edit">
<AdminPanel />
</ProtectedRoute>
}
/>
<Route
path="/users"
element={
<ProtectedRoute permission="user:read">
<UserManagement />
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
);
}
Role-Based Route Protection
const RoleProtectedRoute = ({ roles, children }) => {
const { user, hasAnyRole } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
if (!hasAnyRole(roles)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
// Usage
<Route
path="/admin"
element={
<RoleProtectedRoute roles={['admin']}>
<AdminPanel />
</RoleProtectedRoute>
}
/>
<Route
path="/management"
element={
<RoleProtectedRoute roles={['admin', 'manager']}>
<ManagementPanel />
</RoleProtectedRoute>
}
/>
UI Element Visibility
Hide/Show Based on Permissions
const ActionButtons = ({ userId }) => {
const { hasPermission } = useAuth();
return (
<div className="action-buttons">
{hasPermission('user:update') && (
<button onClick={() => handleEdit(userId)}>Edit</button>
)}
{hasPermission('user:delete') && (
<button onClick={() => handleDelete(userId)}>Delete</button>
)}
{hasPermission('user:read') && (
<button onClick={() => handleView(userId)}>View Details</button>
)}
</div>
);
};
Disable Elements Based on Permissions
const EditForm = () => {
const canEdit = usePermission('content:update');
return (
<form>
<input
type="text"
name="title"
disabled={!canEdit}
/>
<textarea
name="content"
disabled={!canEdit}
/>
<button type="submit" disabled={!canEdit}>
Save Changes
</button>
</form>
);
};
API Request Authorization
Adding Authorization Headers
const apiClient = {
get: async (url, options = {}) => {
const token = localStorage.getItem('authToken');
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (response.status === 403) {
throw new Error('You do not have permission to perform this action');
}
return response.json();
},
post: async (url, data, options = {}) => {
const token = localStorage.getItem('authToken');
const response = await fetch(url, {
method: 'POST',
...options,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
body: JSON.stringify(data),
});
if (response.status === 403) {
throw new Error('You do not have permission to perform this action');
}
return response.json();
},
};
Permission-Based API Calls
const useUserActions = () => {
const { hasPermission } = useAuth();
const deleteUser = async (userId) => {
if (!hasPermission('user:delete')) {
throw new Error('You do not have permission to delete users');
}
return apiClient.delete(`/api/users/${userId}`);
};
const updateUser = async (userId, data) => {
if (!hasPermission('user:update')) {
throw new Error('You do not have permission to update users');
}
return apiClient.put(`/api/users/${userId}`, data);
};
return { deleteUser, updateUser };
};
Dynamic Navigation Based on Roles
Navigation Menu with RBAC
const Navigation = () => {
const { user, hasPermission, hasAnyRole } = useAuth();
const menuItems = [
{
label: 'Dashboard',
path: '/dashboard',
show: true,
},
{
label: 'Users',
path: '/users',
show: hasPermission('user:read'),
},
{
label: 'Content',
path: '/content',
show: hasPermission('content:read'),
},
{
label: 'Settings',
path: '/settings',
show: hasPermission('settings:view'),
},
{
label: 'Admin Panel',
path: '/admin',
show: hasAnyRole(['admin']),
},
];
return (
<nav>
<ul>
{menuItems.filter(item => item.show).map(item => (
<li key={item.path}>
<a href={item.path}>{item.label}</a>
</li>
))}
</ul>
</nav>
);
};
Advanced RBAC Patterns
Resource-Based Permissions
const canAccessResource = (user, resource, action) => {
// Check if user owns the resource
if (resource.ownerId === user.id) {
return true;
}
// Check role-based permission
const permission = `${resource.type}:${action}`;
return user.permissions.includes(permission);
};
// Usage
const EditButton = ({ post }) => {
const { user } = useAuth();
const canEdit = canAccessResource(user, post, 'update');
if (!canEdit) return null;
return <button onClick={() => handleEdit(post)}>Edit</button>;
};
Context-Based Permissions
const hasContextualPermission = (user, action, context) => {
// Check if user has permission in specific context
if (context.department === user.department) {
return user.permissions.includes(action);
}
// Check if user has global permission
return user.permissions.includes(`global:${action}`);
};
Permission Inheritance
const getInheritedPermissions = (role) => {
const roleHierarchy = {
admin: ['admin', 'manager', 'user', 'guest'],
manager: ['manager', 'user', 'guest'],
user: ['user', 'guest'],
guest: ['guest'],
};
const inheritedRoles = roleHierarchy[role] || [role];
const allPermissions = new Set();
inheritedRoles.forEach(inheritedRole => {
const permissions = rolePermissions[inheritedRole] || [];
permissions.forEach(permission => allPermissions.add(permission));
});
return Array.from(allPermissions);
};
Permission Caching
import { useState, useEffect } from 'react';
const usePermissionCache = () => {
const [permissionCache, setPermissionCache] = useState({});
const checkPermission = (permission) => {
if (permission in permissionCache) {
return permissionCache[permission];
}
// Check permission from user context
const hasPermission = checkUserPermission(permission);
// Cache the result
setPermissionCache(prev => ({
...prev,
[permission]: hasPermission,
}));
return hasPermission;
};
return { checkPermission };
};
Error Handling
Unauthorized Access Handler
const UnauthorizedPage = () => {
return (
<div className="unauthorized-page">
<h1>Access Denied</h1>
<p>You do not have permission to access this page.</p>
<a href="/dashboard">Go to Dashboard</a>
</div>
);
};
Permission Error Boundary
import { Component } from 'react';
class PermissionErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
if (error.message.includes('permission')) {
return { hasError: true, error };
}
return null;
}
render() {
if (this.state.hasError) {
return (
<div className="permission-error">
<h2>Permission Error</h2>
<p>{this.state.error.message}</p>
</div>
);
}
return this.props.children;
}
}
Best Practices
1. Principle of Least Privilege
Grant users only the permissions they need to perform their job.
// ✅ Good - Specific permissions
const editorPermissions = ['content:create', 'content:update', 'content:read'];
// ❌ Bad - Too broad
const editorPermissions = ['content:*'];
2. Server-Side Validation
Always validate permissions on the server, not just the client.
// Client-side (UI hiding)
{hasPermission('user:delete') && <DeleteButton />}
// Server-side (enforcement)
app.delete('/api/users/:id', requirePermission('user:delete'), (req, res) => {
// Delete user logic
});
3. Use Permission Constants
Avoid hardcoding permission strings.
// ✅ Good
import { PERMISSIONS } from './constants';
const canDelete = hasPermission(PERMISSIONS.USER_DELETE);
// ❌ Bad
const canDelete = hasPermission('user:delete');
4. Centralize Permission Logic
Keep permission checking logic in one place.
// permissions.js
export const checkPermission = (user, permission) => {
return user?.permissions?.includes(permission) || false;
};
// Use everywhere
import { checkPermission } from './permissions';
Testing RBAC
import { render, screen } from '@testing-library/react';
import { AuthProvider } from './AuthContext';
test('admin can see delete button', () => {
const adminUser = {
role: 'admin',
permissions: ['user:delete'],
};
render(
<AuthProvider initialUser={adminUser}>
<UserManagement />
</AuthProvider>
);
expect(screen.getByText('Delete User')).toBeInTheDocument();
});
test('regular user cannot see delete button', () => {
const regularUser = {
role: 'user',
permissions: ['user:read'],
};
render(
<AuthProvider initialUser={regularUser}>
<UserManagement />
</AuthProvider>
);
expect(screen.queryByText('Delete User')).not.toBeInTheDocument();
});