Permissions & Role-Based Access Control
Overview
PadawanForge implements a comprehensive permissions and role-based access control (RBAC) system that provides granular access control across all application features. This system supports role-based permissions, dynamic permission checking, and administrative access management.
Architecture
Core Components
- PermissionManager: Central permission management engine
- Role System: Predefined and custom roles
- Permission Checking: Dynamic permission validation
- Access Control: Route and feature protection
- Admin Tools: Administrative permission management
Permission Features
- Role-Based Access: Assign permissions to roles
- Granular Permissions: Fine-grained access control
- Dynamic Checking: Runtime permission validation
- Admin Override: Administrative access capabilities
- Audit Logging: Permission change tracking
Implementation
PermissionManager Class
import { PermissionManager, Permission, Role } from '@/lib/utils/permissions';
// Get singleton instance
const permissionManager = PermissionManager.getInstance();
// Check if user has permission
const hasPermission = await permissionManager.hasPermission(
playerUuid,
'admin:users:read'
);
// Check if user has any of multiple permissions
const hasAnyPermission = await permissionManager.hasAnyPermission(
playerUuid,
['admin:users:read', 'admin:users:write']
);
// Check if user has all permissions
const hasAllPermissions = await permissionManager.hasAllPermissions(
playerUuid,
['admin:users:read', 'admin:users:write', 'admin:users:delete']
);
Permission Interface
interface Permission {
id: string; // Unique permission ID
name: string; // Human-readable name
description: string; // Permission description
category: string; // Permission category
resource: string; // Resource being accessed
action: string; // Action being performed
roles: string[]; // Roles that have this permission
}
interface Role {
id: string; // Unique role ID
name: string; // Role name
description: string; // Role description
permissions: string[]; // Permission IDs
isSystem: boolean; // System role flag
isActive: boolean; // Active status
}
Permission Categories
System Permissions
// Core system permissions
const systemPermissions = {
// User management
'admin:users:read': 'Read user information',
'admin:users:write': 'Modify user information',
'admin:users:delete': 'Delete users',
'admin:users:create': 'Create new users',
// Role management
'admin:roles:read': 'Read role information',
'admin:roles:write': 'Modify roles',
'admin:roles:delete': 'Delete roles',
'admin:roles:create': 'Create new roles',
// System configuration
'admin:config:read': 'Read system configuration',
'admin:config:write': 'Modify system configuration',
'admin:system:maintenance': 'Perform system maintenance',
// Analytics and monitoring
'admin:analytics:read': 'Access analytics data',
'admin:logs:read': 'Access system logs',
'admin:monitoring:read': 'Access monitoring data'
};
Game Permissions
// Game-specific permissions
const gamePermissions = {
// Game sessions
'game:sessions:create': 'Create game sessions',
'game:sessions:join': 'Join game sessions',
'game:sessions:moderate': 'Moderate game sessions',
'game:sessions:delete': 'Delete game sessions',
// NPC management
'npc:create': 'Create NPCs',
'npc:edit': 'Edit NPCs',
'npc:delete': 'Delete NPCs',
'npc:assign': 'Assign NPCs to games',
// Chat and communication
'chat:send': 'Send chat messages',
'chat:moderate': 'Moderate chat',
'chat:delete': 'Delete chat messages',
'chat:ban': 'Ban users from chat'
};
Content Permissions
// Content management permissions
const contentPermissions = {
// Statements and content
'content:statements:create': 'Create educational statements',
'content:statements:edit': 'Edit statements',
'content:statements:delete': 'Delete statements',
'content:statements:approve': 'Approve statements',
// Knowledge files
'content:files:upload': 'Upload knowledge files',
'content:files:edit': 'Edit knowledge files',
'content:files:delete': 'Delete knowledge files',
'content:files:share': 'Share knowledge files'
};
Role System
Predefined Roles
// System roles
const systemRoles = {
'super_admin': {
name: 'Super Administrator',
description: 'Full system access',
permissions: Object.keys(systemPermissions),
isSystem: true
},
'admin': {
name: 'Administrator',
description: 'Administrative access',
permissions: [
'admin:users:read',
'admin:users:write',
'admin:config:read',
'admin:analytics:read',
'admin:logs:read'
],
isSystem: true
},
'moderator': {
name: 'Moderator',
description: 'Content and user moderation',
permissions: [
'game:sessions:moderate',
'chat:moderate',
'chat:delete',
'chat:ban',
'content:statements:approve'
],
isSystem: true
},
'content_creator': {
name: 'Content Creator',
description: 'Create and edit content',
permissions: [
'content:statements:create',
'content:statements:edit',
'content:files:upload',
'content:files:edit'
],
isSystem: true
},
'player': {
name: 'Player',
description: 'Standard player access',
permissions: [
'game:sessions:join',
'chat:send',
'npc:create'
],
isSystem: true
}
};
Custom Role Management
class RoleManager {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
// Create custom role
async createRole(role: Omit<Role, 'id'>): Promise<Role> {
const roleId = generateUUID();
await this.db.prepare(`
INSERT INTO roles (id, name, description, is_system, is_active, created_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`).bind(roleId, role.name, role.description, role.isSystem, role.isActive).run();
// Add permissions to role
for (const permissionId of role.permissions) {
await this.addPermissionToRole(roleId, permissionId);
}
return { ...role, id: roleId };
}
// Update role
async updateRole(roleId: string, updates: Partial<Role>): Promise<void> {
const updates: string[] = [];
const values: any[] = [];
if (updates.name) {
updates.push('name = ?');
values.push(updates.name);
}
if (updates.description) {
updates.push('description = ?');
values.push(updates.description);
}
if (updates.isActive !== undefined) {
updates.push('is_active = ?');
values.push(updates.isActive);
}
if (updates.length > 0) {
values.push(roleId);
await this.db.prepare(`
UPDATE roles SET ${updates.join(', ')}, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).bind(...values).run();
}
// Update permissions if provided
if (updates.permissions) {
await this.updateRolePermissions(roleId, updates.permissions);
}
}
// Delete role
async deleteRole(roleId: string): Promise<void> {
// Check if role is system role
const role = await this.getRole(roleId);
if (role?.isSystem) {
throw new Error('Cannot delete system roles');
}
// Remove role from all users
await this.db.prepare(`
DELETE FROM player_roles WHERE role_id = ?
`).bind(roleId).run();
// Remove role permissions
await this.db.prepare(`
DELETE FROM role_permissions WHERE role_id = ?
`).bind(roleId).run();
// Delete role
await this.db.prepare(`
DELETE FROM roles WHERE id = ?
`).bind(roleId).run();
}
// Get role with permissions
async getRole(roleId: string): Promise<Role | null> {
const role = await this.db.prepare(`
SELECT id, name, description, is_system, is_active
FROM roles WHERE id = ?
`).bind(roleId).first();
if (!role) return null;
const permissions = await this.db.prepare(`
SELECT permission_id FROM role_permissions WHERE role_id = ?
`).bind(roleId).all();
return {
...role,
permissions: permissions.results.map(p => p.permission_id)
};
}
// List all roles
async listRoles(): Promise<Role[]> {
const roles = await this.db.prepare(`
SELECT id, name, description, is_system, is_active
FROM roles ORDER BY name
`).all();
const rolesWithPermissions = await Promise.all(
roles.results.map(async (role) => {
const permissions = await this.db.prepare(`
SELECT permission_id FROM role_permissions WHERE role_id = ?
`).bind(role.id).all();
return {
...role,
permissions: permissions.results.map(p => p.permission_id)
};
})
);
return rolesWithPermissions;
}
}
Permission Checking
Dynamic Permission Validation
class PermissionChecker {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
// Check if user has specific permission
async hasPermission(playerUuid: string, permissionId: string): Promise<boolean> {
// Check if user has admin override
if (await this.hasAdminOverride(playerUuid)) {
return true;
}
const result = await this.db.prepare(`
SELECT 1 FROM player_roles pr
JOIN role_permissions rp ON pr.role_id = rp.role_id
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND rp.permission_id = ?
AND r.is_active = true
`).bind(playerUuid, permissionId).first();
return Boolean(result);
}
// Check if user has any of the specified permissions
async hasAnyPermission(playerUuid: string, permissionIds: string[]): Promise<boolean> {
if (permissionIds.length === 0) return false;
// Check if user has admin override
if (await this.hasAdminOverride(playerUuid)) {
return true;
}
const placeholders = permissionIds.map(() => '?').join(',');
const result = await this.db.prepare(`
SELECT 1 FROM player_roles pr
JOIN role_permissions rp ON pr.role_id = rp.role_id
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND rp.permission_id IN (${placeholders})
AND r.is_active = true
LIMIT 1
`).bind(playerUuid, ...permissionIds).first();
return Boolean(result);
}
// Check if user has all specified permissions
async hasAllPermissions(playerUuid: string, permissionIds: string[]): Promise<boolean> {
if (permissionIds.length === 0) return true;
// Check if user has admin override
if (await this.hasAdminOverride(playerUuid)) {
return true;
}
const placeholders = permissionIds.map(() => '?').join(',');
const result = await this.db.prepare(`
SELECT COUNT(DISTINCT rp.permission_id) as count
FROM player_roles pr
JOIN role_permissions rp ON pr.role_id = rp.role_id
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND rp.permission_id IN (${placeholders})
AND r.is_active = true
`).bind(playerUuid, ...permissionIds).first();
return result?.count === permissionIds.length;
}
// Check admin override
private async hasAdminOverride(playerUuid: string): Promise<boolean> {
const result = await this.db.prepare(`
SELECT 1 FROM player_roles pr
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND r.name IN ('super_admin', 'admin')
AND r.is_active = true
`).bind(playerUuid).first();
return Boolean(result);
}
// Get user's permissions
async getUserPermissions(playerUuid: string): Promise<string[]> {
const result = await this.db.prepare(`
SELECT DISTINCT rp.permission_id
FROM player_roles pr
JOIN role_permissions rp ON pr.role_id = rp.role_id
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND r.is_active = true
`).bind(playerUuid).all();
return result.results.map(r => r.permission_id);
}
// Get user's roles
async getUserRoles(playerUuid: string): Promise<Role[]> {
const result = await this.db.prepare(`
SELECT r.id, r.name, r.description, r.is_system, r.is_active
FROM player_roles pr
JOIN roles r ON pr.role_id = r.id
WHERE pr.player_uuid = ?
AND r.is_active = true
`).bind(playerUuid).all();
return result.results;
}
}
Access Control Integration
Route Protection
// Protect API routes with permissions
function requirePermission(permissionId: string) {
return async (request: Request, locals: any) => {
const playerUuid = locals.player?.uuid;
if (!playerUuid) {
throw new Error('Authentication required');
}
const permissionChecker = new PermissionChecker(locals.runtime.env.DB);
const hasPermission = await permissionChecker.hasPermission(playerUuid, permissionId);
if (!hasPermission) {
throw new Error('Insufficient permissions');
}
};
}
// Usage in API routes
export async function GET(request: Request, locals: any) {
// Check permission before processing
await requirePermission('admin:users:read')(request, locals);
// Process request
const users = await getUsers();
return new Response(JSON.stringify({ users }));
}
Component Protection
// React component with permission checking
import { useState, useEffect } from 'react';
import { checkPermission } from '@/lib/utils/permissions';
interface ProtectedComponentProps {
permission: string;
fallback?: React.ReactNode;
children: React.ReactNode;
}
function ProtectedComponent({ permission, fallback, children }: ProtectedComponentProps) {
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function checkUserPermission() {
try {
const result = await checkPermission(permission);
setHasPermission(result);
} catch (error) {
console.error('Permission check failed:', error);
setHasPermission(false);
} finally {
setLoading(false);
}
}
checkUserPermission();
}, [permission]);
if (loading) {
return <div>Checking permissions...</div>;
}
if (!hasPermission) {
return fallback || <div>Access denied</div>;
}
return <>{children}</>;
}
// Usage
function AdminPanel() {
return (
<ProtectedComponent permission="admin:users:read">
<div className="admin-panel">
<h2>User Management</h2>
<UserList />
</div>
</ProtectedComponent>
);
}
Middleware Integration
import { defineMiddleware } from 'astro:middleware';
import { PermissionChecker } from '@/lib/utils/permissions';
export const onRequest = defineMiddleware(async ({ request, locals, next }) => {
// Skip permission check for public routes
const publicRoutes = ['/api/health', '/api/auth', '/api/public'];
const url = new URL(request.url);
if (publicRoutes.some(route => url.pathname.startsWith(route))) {
return next();
}
// Check authentication
const playerUuid = locals.player?.uuid;
if (!playerUuid) {
return new Response('Unauthorized', { status: 401 });
}
// Check permissions for admin routes
if (url.pathname.startsWith('/api/admin')) {
const permissionChecker = new PermissionChecker(locals.runtime.env.DB);
const hasAdminPermission = await permissionChecker.hasAnyPermission(playerUuid, [
'admin:users:read',
'admin:config:read',
'admin:analytics:read'
]);
if (!hasAdminPermission) {
return new Response('Forbidden', { status: 403 });
}
}
return next();
});
Administrative Tools
Permission Management UI
// Admin permission management component
function PermissionManagement() {
const [roles, setRoles] = useState<Role[]>([]);
const [permissions, setPermissions] = useState<Permission[]>([]);
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
useEffect(() => {
loadRoles();
loadPermissions();
}, []);
const handleRolePermissionUpdate = async (roleId: string, permissionIds: string[]) => {
try {
await updateRolePermissions(roleId, permissionIds);
await loadRoles();
} catch (error) {
console.error('Failed to update role permissions:', error);
}
};
return (
<div className="permission-management">
<div className="roles-panel">
<h3>Roles</h3>
{roles.map(role => (
<div
key={role.id}
className={`role-item ${selectedRole?.id === role.id ? 'selected' : ''}`}
onClick={() => setSelectedRole(role)}
>
<span className="role-name">{role.name}</span>
<span className="role-description">{role.description}</span>
{role.isSystem && <span className="system-badge">System</span>}
</div>
))}
</div>
{selectedRole && (
<div className="permissions-panel">
<h3>Permissions for {selectedRole.name}</h3>
<PermissionSelector
permissions={permissions}
selectedPermissions={selectedRole.permissions}
onPermissionsChange={(permissionIds) =>
handleRolePermissionUpdate(selectedRole.id, permissionIds)
}
disabled={selectedRole.isSystem}
/>
</div>
)}
</div>
);
}
User Role Management
// User role assignment component
function UserRoleManagement() {
const [users, setUsers] = useState<any[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [selectedUser, setSelectedUser] = useState<any | null>(null);
const handleUserRoleUpdate = async (playerUuid: string, roleIds: string[]) => {
try {
await updateUserRoles(playerUuid, roleIds);
await loadUsers();
} catch (error) {
console.error('Failed to update user roles:', error);
}
};
return (
<div className="user-role-management">
<div className="users-panel">
<h3>Users</h3>
{users.map(user => (
<div
key={user.uuid}
className={`user-item ${selectedUser?.uuid === user.uuid ? 'selected' : ''}`}
onClick={() => setSelectedUser(user)}
>
<span className="user-name">{user.displayName}</span>
<span className="user-email">{user.email}</span>
</div>
))}
</div>
{selectedUser && (
<div className="user-roles-panel">
<h3>Roles for {selectedUser.displayName}</h3>
<RoleSelector
roles={roles}
selectedRoles={selectedUser.roles}
onRolesChange={(roleIds) =>
handleUserRoleUpdate(selectedUser.uuid, roleIds)
}
/>
</div>
)}
</div>
);
}
Audit Logging
Permission Change Tracking
// Track permission changes
class PermissionAuditLogger {
private db: D1Database;
constructor(db: D1Database) {
this.db = db;
}
// Log role permission changes
async logRolePermissionChange(
adminUuid: string,
roleId: string,
permissionId: string,
action: 'grant' | 'revoke'
): Promise<void> {
await this.db.prepare(`
INSERT INTO permission_audit_log (
admin_uuid, role_id, permission_id, action, timestamp
) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`).bind(adminUuid, roleId, permissionId, action).run();
}
// Log user role changes
async logUserRoleChange(
adminUuid: string,
playerUuid: string,
roleId: string,
action: 'assign' | 'remove'
): Promise<void> {
await this.db.prepare(`
INSERT INTO role_audit_log (
admin_uuid, player_uuid, role_id, action, timestamp
) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
`).bind(adminUuid, playerUuid, roleId, action).run();
}
// Get audit log
async getAuditLog(limit: number = 100): Promise<any[]> {
const result = await this.db.prepare(`
SELECT
'permission' as type,
admin_uuid,
role_id,
permission_id as target_id,
action,
timestamp
FROM permission_audit_log
UNION ALL
SELECT
'role' as type,
admin_uuid,
role_id,
player_uuid as target_id,
action,
timestamp
FROM role_audit_log
ORDER BY timestamp DESC
LIMIT ?
`).bind(limit).all();
return result.results;
}
}
Testing
Permission System Testing
describe('Permission System', () => {
let permissionChecker: PermissionChecker;
let roleManager: RoleManager;
beforeEach(() => {
permissionChecker = new PermissionChecker(mockDb);
roleManager = new RoleManager(mockDb);
});
it('should check permissions correctly', async () => {
// Mock user with admin role
jest.spyOn(permissionChecker, 'hasPermission')
.mockResolvedValue(true);
const hasPermission = await permissionChecker.hasPermission('user123', 'admin:users:read');
expect(hasPermission).toBe(true);
});
it('should handle admin override', async () => {
// Mock admin override
jest.spyOn(permissionChecker, 'hasAdminOverride')
.mockResolvedValue(true);
const hasPermission = await permissionChecker.hasPermission('admin123', 'any:permission');
expect(hasPermission).toBe(true);
});
it('should check multiple permissions', async () => {
jest.spyOn(permissionChecker, 'hasAnyPermission')
.mockResolvedValue(true);
const hasAnyPermission = await permissionChecker.hasAnyPermission('user123', [
'admin:users:read',
'admin:users:write'
]);
expect(hasAnyPermission).toBe(true);
});
});
This comprehensive permissions system provides robust access control and role management across the entire PadawanForge application.