// Route permissions are enforced before handlers, including routes installed early. // Permissions from a session payload are never accepted as authority. export const adminRoles=Object.freeze({ admin:Object.freeze(['evidence.read','incidents.read','incidents.triage','overview.read','audit.read','policy.read','policy.prepare','policy.write','devices.manage','bookmarks.read','bookmarks.write','teaching.read','teaching.write','integrations.test','sessions.manage']), operator:Object.freeze(['incidents.read','incidents.triage','overview.read','audit.read','policy.read','policy.prepare','devices.manage','bookmarks.read','bookmarks.write','teaching.read','teaching.write','integrations.test']), auditor:Object.freeze(['incidents.read','overview.read','audit.read','policy.read','teaching.read']) }); export function bootstrapAllowed(config){ try{const url=new URL(config.publicUrl);return url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname);}catch{return false;} } export function normalizePrincipal(principal){ const role=principal.role??(principal.admin===true?'admin':null); if(!Object.hasOwn(adminRoles,role))return null; return {sub:principal.sub,name:principal.name,admin:role==='admin',role,permissions:[...adminRoles[role]]}; } export function principalForGroups(identity,groups,config){ const list=Array.isArray(groups)?groups:[]; const role=list.includes(config.oidcAdminGroup)?'admin':config.oidcOperatorGroup&&list.includes(config.oidcOperatorGroup)?'operator':config.oidcAuditorGroup&&list.includes(config.oidcAuditorGroup)?'auditor':null; return role?normalizePrincipal({...identity,role}):{...identity,admin:false}; } export function adminRoutePermission(method,url){ const path=new URL(url,'http://localhost').pathname; if(method==='GET'){ if(/^\/api\/admin\/evidence\/[^/]+$/.test(path))return 'evidence.read'; if(/^\/api\/admin\/incidents(?:\/[^/]+)?$/.test(path))return 'incidents.read'; if(path==='/api/admin/user-profiles')return 'policy.read'; if(path==='/api/admin/policy-groups')return 'policy.read'; if(/^\/api\/admin\/policy-reviews(?:\/[^/]+)?$/.test(path))return 'policy.read'; if(path==='/api/admin/policy/history'||/^\/api\/admin\/policy\/history\/\d+$/.test(path)||path==='/api/admin/policy/diff')return 'policy.read'; if(path==='/api/admin/sessions')return 'sessions.manage'; if(path==='/api/admin/overview'||path==='/api/admin/session')return 'overview.read'; if(path==='/api/admin/events/export')return 'audit.read'; if(path==='/api/admin/teaching')return 'teaching.read'; if(/^\/api\/admin\/devices\/[^/]+\/bookmarks$/.test(path))return 'bookmarks.read'; if(['/api/admin/management-channel','/api/admin/presets','/api/admin/integrations'].includes(path))return 'policy.read'; } if(method==='POST'&&path==='/api/admin/incidents')return 'incidents.triage'; if(method==='PATCH'&&/^\/api\/admin\/incidents\/[^/]+$/.test(path))return 'incidents.triage'; if(method==='POST'&&path==='/api/admin/policy-reviews')return 'policy.prepare'; if(method==='POST'&&path==='/api/admin/user-profiles/preview')return 'policy.read'; if(method==='PUT'&&path==='/api/admin/user-profile-configuration')return 'policy.write'; if(['PUT','DELETE'].includes(method)&&/^\/api\/admin\/user-profiles\/[^/]+$/.test(path))return 'policy.write'; if(method==='POST'&&path==='/api/admin/policy-groups/preview')return 'policy.read'; if(['PUT','DELETE'].includes(method)&&/^\/api\/admin\/policy-groups\/[^/]+$/.test(path))return 'policy.write'; if(method==='POST'&&/^\/api\/admin\/policy-reviews\/[^/]+\/(approve|reject)$/.test(path))return 'policy.write'; if(method==='POST'&&path==='/api/admin/policy/restore')return 'policy.write'; if(method==='POST'&&/^\/api\/admin\/sessions\/[^/]+\/revoke$/.test(path))return 'sessions.manage'; if(method==='PUT'&&/^\/api\/admin\/devices\/[^/]+\/bookmarks$/.test(path))return 'bookmarks.write'; if(method==='POST'&&(path==='/api/admin/enrollments'||/^\/api\/admin\/devices\/[^/]+\/revoke$/.test(path)))return 'devices.manage'; // Per-device inspection changes publish the shared policy version. if(method==='PATCH'&&/^\/api\/admin\/devices\/[^/]+\/inspection$/.test(path))return 'policy.write'; if(method==='PUT'&&['/api/admin/policy','/api/admin/security/blocklists'].includes(path))return 'policy.write'; if(method==='POST'&&/^\/api\/admin\/teaching\/[^/]+\/publish$/.test(path))return 'policy.write'; if(method==='POST'&&path==='/api/admin/integrations/icap/probe')return 'integrations.test'; if(method==='POST'&&['/api/admin/teaching','/api/admin/semantic/reference','/api/admin/semantic/document-reference','/api/admin/security/visual-reference'].includes(path))return 'teaching.write'; if(method==='POST'&&['/api/admin/policy/test','/api/admin/presets/preview'].includes(path))return 'policy.read'; return null; // New endpoints require an explicit permission; never infer from HTTP method. }