Skip to main content

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​

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();
});