Add native host management and role-based operator authentication
Introduce an outbound Python host agent, Debian installer, durable remote operations, immutable game-file libraries, host-aware deployments and ports, and occupancy-safe campaigns and draining. Native hosts do not require Komodo. Add administrator, operator and viewer accounts, protected management routes, secure sessions, password recovery, audit history and separate telemetry tokens. Document first-admin setup, reporter migration and live-host acceptance checks. Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type checking, the production build, compiled SSR/API smoke tests and administrator recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { UserRole } from '../../auth.js';
|
||||
|
||||
export type RoutePermission = 'public' | 'machine' | 'authenticated' | 'viewer' | 'operator' | 'admin';
|
||||
|
||||
const within = (route: string, base: string): boolean => route === base || route.startsWith(base + '/');
|
||||
const readMethod = (method: string): boolean => method === 'GET' || method === 'HEAD';
|
||||
|
||||
/** Classify SvelteKit's resolved route ID, never a user-supplied URL pathname.
|
||||
* Route IDs preserve the same policy for page-data requests and encoded URLs.
|
||||
* Machine endpoints enforce their own host/enrollment/telemetry credentials.
|
||||
*/
|
||||
export function routePermission(routeId: string | null, method: string): RoutePermission {
|
||||
method = method.toUpperCase();
|
||||
const reading = readMethod(method);
|
||||
if (routeId === null) return reading ? 'viewer' : 'operator';
|
||||
// These cannot occur in this application's resolved route IDs. Fail closed
|
||||
// if a future caller accidentally supplies a raw or noncanonical pathname.
|
||||
if (!routeId.startsWith('/') || /[%\\?#\x00-\x1f\x7f]/.test(routeId)
|
||||
|| routeId !== '/' && routeId.split('/').slice(1).some(part => !part || part === '.' || part === '..')) return 'admin';
|
||||
|
||||
if (within(routeId, '/api/agent')) return 'machine';
|
||||
if (routeId === '/api/telemetry' && method === 'POST') return 'machine';
|
||||
if ((routeId === '/login' || routeId === '/setup') && reading) return 'public';
|
||||
if ((routeId === '/api/auth/login' || routeId === '/api/auth/setup') && method === 'POST') return 'public';
|
||||
if (routeId === '/account' || ['/api/auth/me', '/api/auth/logout', '/api/auth/password'].includes(routeId)) return 'authenticated';
|
||||
|
||||
if (within(routeId, '/api/users') || within(routeId, '/settings') || within(routeId, '/api/auth')
|
||||
|| within(routeId, '/api/server-keys') || within(routeId, '/server-keys') || within(routeId, '/keys')) return 'admin';
|
||||
|
||||
if (!reading && ['/api/hosts', '/api/settings', '/api/game-files'].some(base => within(routeId, base))) return 'admin';
|
||||
// These endpoints mix operational actions with global policy and schedule
|
||||
// changes. Per-incident/job actions remain available to operators.
|
||||
if (!reading && ['/api/maintenance', '/api/plutonium', '/api/watchdog'].includes(routeId)) return 'admin';
|
||||
|
||||
// Summaries remain visible to viewers; raw configuration, historical file
|
||||
// bytes, and unfiltered diagnostics may contain runtime credentials.
|
||||
if (routeId === '/configs/[id]' || routeId === '/api/configs/[id]' || routeId === '/api/configs/inspect'
|
||||
|| routeId === '/api/campaigns/[id]/file/[fileId]' || routeId === '/api/deployments/[id]/file/[fileId]'
|
||||
|| routeId === '/api/servers/[name]/logs/[file]' || routeId === '/api/servers/[name]/telemetry-requests') return 'operator';
|
||||
|
||||
return reading ? 'viewer' : 'operator';
|
||||
}
|
||||
|
||||
/** A browser role never substitutes for a machine credential. */
|
||||
export function roleAllows(role: UserRole, permission: RoutePermission): boolean {
|
||||
if (!['viewer', 'operator', 'admin'].includes(role)) return false;
|
||||
if (permission === 'machine') return false;
|
||||
if (permission === 'public' || permission === 'authenticated' || permission === 'viewer') return true;
|
||||
if (permission === 'operator') return role === 'operator' || role === 'admin';
|
||||
return permission === 'admin' && role === 'admin';
|
||||
}
|
||||
Reference in New Issue
Block a user