UNPKG

mcp-product-manager

Version:

MCP Orchestrator for task and project management with web interface

349 lines 14.9 kB
// rbac.js - Role-Based Access Control middleware for MCP tool access // Role definitions with hierarchical permissions const ROLES = { ADMIN: { level: 4, description: 'Full system access - all tools and operations', permissions: ['*'] // Wildcard permission }, ORCHESTRATOR: { level: 3, description: 'Orchestration and management operations', permissions: ['*'] // Temporary - give all permissions to test SPAPS wizard }, AGENT: { level: 2, description: 'Task execution and basic operations', permissions: [ 'tasks:claim', 'tasks:complete', 'tasks:update-status', 'tasks:report-tokens', 'tasks:detail', 'tasks:list', 'projects:status', 'bundles:detail', 'bundles:claim', 'agents:status', 'mcp:execute', 'usage:current' ] }, VIEWER: { level: 1, description: 'Read-only access to system state', permissions: [ 'tasks:list', 'tasks:detail', 'projects:list', 'projects:status', 'bundles:detail', 'agents:list', 'health:check', 'usage:current' ] }, ANONYMOUS: { level: 0, description: 'No access', permissions: ['health:check'] } }; // Tool permission matrix mapping endpoints to required permissions const TOOL_PERMISSIONS = { // Task operations 'POST /api/tasks': 'tasks:create', 'GET /api/tasks/:project': 'tasks:list', 'GET /api/tasks/detail/:id': 'tasks:detail', 'PATCH /api/tasks/:id': 'tasks:update', 'POST /api/tasks/:id/claim': 'tasks:claim', 'POST /api/tasks/:id/complete': 'tasks:complete', 'POST /api/tasks/:id/review': 'tasks:review', 'PUT /api/tasks/:id/status': 'tasks:update-status', 'PATCH /api/tasks/:id/priority': 'tasks:priority', 'GET /api/tasks/blockers/:project': 'tasks:blockers', 'GET /api/tasks/performance/:project': 'tasks:performance', 'POST /api/tasks/cleanup': 'tasks:cleanup', 'POST /api/tasks/:id/tokens': 'tasks:report-tokens', // Project operations 'POST /api/projects': 'projects:create', 'GET /api/projects': 'projects:list', 'GET /api/projects/summaries': 'projects:summaries', 'GET /api/projects/:name/status': 'projects:status', 'POST /api/projects/:name/merge': 'projects:merge', 'POST /api/projects/:name/recalibrate': 'projects:recalibrate', 'GET /api/projects/:project/purpose': 'projects:purpose', 'PUT /api/projects/:project/purpose': 'projects:purpose', 'GET /api/projects/:project/checklist': 'projects:checklist', 'PUT /api/projects/:project/checklist': 'projects:checklist', // Bundle operations 'POST /api/bundles/analyze': 'bundles:analyze', 'POST /api/bundles': 'bundles:create', 'GET /api/bundles/:id': 'bundles:detail', 'POST /api/bundles/:id/claim': 'bundles:claim', 'GET /api/bundles/recommendations': 'bundles:recommendations', 'GET /api/bundles/recommendations/:project': 'bundles:recommendations', // Orchestrator operations 'POST /api/orchestrator/next-action': 'orchestrator:next-action', 'POST /api/orchestrator/orchestrate': 'orchestrator:orchestrate', // Agent operations 'POST /api/agents/spawn': 'agents:spawn', 'GET /api/agents': 'agents:list', 'GET /api/agents/:name/status': 'agents:status', 'GET /api/agents/:name/scorecard': 'agents:scorecard', 'GET /api/agents/instructions': 'agents:instructions', 'GET /api/agents/instructions/:category': 'agents:instructions', // Database operations 'POST /api/database/execute': 'database:execute', // MCP operations 'POST /api/mcp/execute': 'mcp:execute', 'GET /api/mcp/tools': 'mcp:tools', // Usage operations 'GET /api/usage/current': 'usage:current', 'GET /api/usage/trends': 'usage:trends', 'GET /api/usage/snapshot': 'usage:snapshot', 'POST /api/usage/snapshot': 'usage:snapshot', 'GET /api/usage/budget': 'usage:budget', // SPAPS Integration Wizard 'POST /api/spaps/wizard': 'spaps:wizard', 'POST /spaps/wizard': 'spaps:wizard', // Health check 'GET /api/health': 'health:check', 'GET /api': 'health:check' }; // Extract role from request (multiple strategies) const extractRole = (req) => { // Strategy 1: X-Role header (for testing/development) if (req.headers['x-role']) { const role = req.headers['x-role'].toUpperCase(); if (ROLES[role]) { return role; } } // Strategy 2: MCP tools get ORCHESTRATOR access const userAgent = req.headers['user-agent'] || ''; if (userAgent.includes('mcp-terminal') || userAgent.includes('claude-mcp') || userAgent.includes('axios')) { // Check if it's from the MCP orchestrator tools by path const path = req.path.toLowerCase(); if (path.includes('/orchestrator') || path.includes('/bundles') || path.includes('/tasks') || path.includes('/projects') || path.includes('/agents')) { return 'ORCHESTRATOR'; } } // Strategy 2: Authorization header parsing if (req.headers.authorization) { const auth = req.headers.authorization; if (auth.startsWith('Bearer ') && auth.includes('role=')) { const roleMatch = auth.match(/role=([^&]+)/); if (roleMatch) { const role = roleMatch[1].toUpperCase(); if (ROLES[role]) { return role; } } } } // Strategy 3: Query parameter (lowest priority) if (req.query.role) { const role = req.query.role.toUpperCase(); if (ROLES[role]) { return role; } } // Strategy 4: Default based on endpoint patterns const path = req.path; const agent = req.headers['x-agent'] || req.body?.agent; if (agent && agent.includes('orchestrator')) { return 'ORCHESTRATOR'; } if (path.includes('/orchestrator/')) { return 'ORCHESTRATOR'; } // Only infer AGENT role if there's an agent header/body AND specific agent paths if (agent && (path.includes('/agents/') || path.includes('/tasks/'))) { return 'AGENT'; } // Default to VIEWER for read operations, ANONYMOUS for others if (req.method === 'GET') { return 'VIEWER'; } return 'ANONYMOUS'; }; // Check if role has permission const hasPermission = (role, requiredPermission) => { const roleConfig = ROLES[role]; if (!roleConfig) { return false; } // Admin has all permissions if (roleConfig.permissions.includes('*')) { return true; } // Check for exact permission match if (roleConfig.permissions.includes(requiredPermission)) { return true; } // Check for wildcard permissions (e.g., "tasks:*" matches "tasks:create") const [category] = requiredPermission.split(':'); if (roleConfig.permissions.includes(`${category}:*`)) { return true; } return false; }; // Generate route key from request const getRouteKey = (req) => { const method = req.method; let path = req.path; // Normalize path to always start with /api for consistency if (!path.startsWith('/api')) { path = `/api${path}`; } // Known route patterns to match against const routePatterns = [ // Task routes { pattern: /^\/api\/tasks\/([^\/]+)$/, replacement: '/api/tasks/:project' }, { pattern: /^\/api\/tasks\/detail\/([^\/]+)$/, replacement: '/api/tasks/detail/:id' }, { pattern: /^\/api\/tasks\/([^\/]+)\/claim$/, replacement: '/api/tasks/:id/claim' }, { pattern: /^\/api\/tasks\/([^\/]+)\/complete$/, replacement: '/api/tasks/:id/complete' }, { pattern: /^\/api\/tasks\/([^\/]+)\/review$/, replacement: '/api/tasks/:id/review' }, { pattern: /^\/api\/tasks\/([^\/]+)\/status$/, replacement: '/api/tasks/:id/status' }, { pattern: /^\/api\/tasks\/([^\/]+)\/priority$/, replacement: '/api/tasks/:id/priority' }, { pattern: /^\/api\/tasks\/([^\/]+)\/tokens$/, replacement: '/api/tasks/:id/tokens' }, { pattern: /^\/api\/tasks\/blockers\/([^\/]+)$/, replacement: '/api/tasks/blockers/:project' }, { pattern: /^\/api\/tasks\/performance\/([^\/]+)$/, replacement: '/api/tasks/performance/:project' }, // Project routes { pattern: /^\/api\/projects\/([^\/]+)\/status$/, replacement: '/api/projects/:name/status' }, { pattern: /^\/api\/projects\/([^\/]+)\/merge$/, replacement: '/api/projects/:name/merge' }, { pattern: /^\/api\/projects\/([^\/]+)\/recalibrate$/, replacement: '/api/projects/:name/recalibrate' }, { pattern: /^\/api\/projects\/([^\/]+)\/purpose$/, replacement: '/api/projects/:project/purpose' }, { pattern: /^\/api\/projects\/([^\/]+)\/checklist$/, replacement: '/api/projects/:project/checklist' }, // Bundle routes - specific routes first { pattern: /^\/api\/bundles\/analyze$/, replacement: '/api/bundles/analyze' }, { pattern: /^\/api\/bundles\/create$/, replacement: '/api/bundles/create' }, { pattern: /^\/api\/bundles\/recommendations$/, replacement: '/api/bundles/recommendations' }, { pattern: /^\/api\/bundles\/recommendations\/([^\/]+)$/, replacement: '/api/bundles/recommendations/:project' }, { pattern: /^\/api\/bundles\/([^\/]+)\/claim$/, replacement: '/api/bundles/:id/claim' }, { pattern: /^\/api\/bundles\/([^\/]+)$/, replacement: '/api/bundles/:id' }, // Agent routes { pattern: /^\/api\/agents\/([^\/]+)\/status$/, replacement: '/api/agents/:name/status' }, { pattern: /^\/api\/agents\/([^\/]+)\/scorecard$/, replacement: '/api/agents/:name/scorecard' }, { pattern: /^\/api\/agents\/instructions\/([^\/]+)$/, replacement: '/api/agents/instructions/:category' }, // RBAC routes { pattern: /^\/api\/rbac\/roles\/([^\/]+)$/, replacement: '/api/rbac/roles/:roleName' } ]; // Try to match against known patterns for (const { pattern, replacement } of routePatterns) { if (pattern.test(path)) { return `${method} ${replacement}`; } } // Return normalized path as-is if no pattern matches return `${method} ${path}`; }; // Main RBAC middleware export const rbacMiddleware = (options = {}) => { const { enforceLogging = true, allowDevelopmentBypass = process.env.NODE_ENV === 'development', defaultRole = 'ANONYMOUS' } = options; return (req, res, next) => { // Development bypass if (allowDevelopmentBypass && req.headers['x-rbac-bypass'] === 'true') { req.rbac = { role: 'ADMIN', permissions: ['*'], bypassed: true }; return next(); } // Extract user role const role = extractRole(req) || defaultRole; const roleConfig = ROLES[role]; // Get required permission for this endpoint const routeKey = getRouteKey(req); const requiredPermission = TOOL_PERMISSIONS[routeKey]; // Debug logging for SPAPS wizard if (req.path.includes('/spaps/wizard')) { console.error(`🔍 SPAPS DEBUG: Route key: "${routeKey}"`); console.error(`🔍 SPAPS DEBUG: Required permission: "${requiredPermission}"`); console.error(`🔍 SPAPS DEBUG: Available SPAPS keys:`, Object.keys(TOOL_PERMISSIONS).filter(k => k.includes('spaps'))); } // Debug logging for route key issues if (!requiredPermission && req.path.includes('/api/')) { console.log(`🔍 DEBUG: No permission found for route key: "${routeKey}"`); console.log(`🔍 Available keys:`, Object.keys(TOOL_PERMISSIONS).filter(k => k.includes(req.path.split('/')[2] || ''))); } // If no specific permission defined, default based on method const permission = requiredPermission || (req.method === 'GET' ? 'read' : 'write'); // Check permission const authorized = hasPermission(role, permission); // Attach RBAC info to request req.rbac = { role, roleConfig, requiredPermission: permission, authorized, routeKey }; // Log access attempt if enabled if (enforceLogging) { const logData = { timestamp: new Date().toISOString(), method: req.method, path: req.path, role, permission, authorized, ip: req.ip, userAgent: req.headers['user-agent'], agent: req.headers['x-agent'] || req.body?.agent }; // Store in req for downstream logging middleware req.rbacLog = logData; console.log(`🔐 RBAC [${authorized ? '✅' : '❌'}] ${role}${req.method} ${req.path} (${permission})`); } // Block unauthorized requests if (!authorized) { return res.status(403).json({ success: false, error: 'Access denied', details: { message: `Role '${role}' lacks permission '${permission}' for ${req.method} ${req.path}`, rbac: { role, role_level: roleConfig.level, required_permission: permission, available_permissions: roleConfig.permissions, route_key: routeKey }, suggestion: role === 'ANONYMOUS' ? 'Please provide authentication headers (X-Role, Authorization, or query parameter)' : `This operation requires elevated permissions. Current role: ${role}`, help: 'Contact administrator for role assignment or use X-Role header for testing' } }); } next(); }; }; // Utility function to check permissions programmatically export const checkPermission = (role, permission) => { return hasPermission(role, permission); }; // Get role information export const getRoleInfo = (role) => { return ROLES[role] || null; }; // List all available roles export const listRoles = () => { return Object.entries(ROLES).map(([name, config]) => ({ name, level: config.level, description: config.description, permissions: config.permissions })); }; // Export role constants export { ROLES, TOOL_PERMISSIONS }; export default rbacMiddleware; //# sourceMappingURL=rbac.js.map