Files
safetynet/apps/server/admin-access.mjs
T
2026-10-13 10:34:48 +02:00

60 lines
4.9 KiB
JavaScript

// 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.
}