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.

PadawanForge v1.4.1