import { describe, expect, it } from 'vitest'; import type { UserRole } from '../../src/lib/auth.js'; import { roleAllows, routePermission, type RoutePermission } from '../../src/lib/server/auth/access.js'; const roles: UserRole[] = ['viewer', 'operator', 'admin']; const allows = (role: UserRole, route: string | null, method = 'GET') => roleAllows(role, routePermission(route, method)); describe('management route permissions', () => { it.each([ '/', '/api/fleet', '/capacity', '/api/capacity', '/api/servers', '/servers/[name]', '/api/servers/[name]/scripts', '/api/metrics', '/metrics', '/api/matches', '/matches/[id]', '/leaderboard/players/[game]/[guid]', '/api/campaigns/[id]', '/campaigns/[id]', '/api/provisioning/[id]', '/provisioning/[id]', '/api/provisioning/[id]/events', '/api/betas/[id]', '/api/scripts/[id]', '/api/configs', '/api/plugins', '/api/hosts/[id]', '/hosts/operations', '/api/hosts/operations', '/api/game-files/[id]', '/api/maintenance', '/api/events/servers', '/api/events/pipeline' ])('lets viewers read the safe summary route %s', route => { expect(routePermission(route, 'GET')).toBe('viewer'); expect(allows('viewer', route)).toBe(true); expect(allows('viewer', route, 'HEAD')).toBe(true); }); it.each([ ['/api/servers', 'POST'], ['/api/servers/[name]', 'DELETE'], ['/api/provisioning', 'POST'], ['/api/provisioning/[id]/cancel', 'POST'], ['/api/provisioning/[id]/resume', 'POST'], ['/api/deployment-collections', 'POST'], ['/api/deployment-collections/preview', 'POST'], ['/api/blueprints', 'POST'], ['/api/blueprints/[id]', 'PUT'], ['/api/blueprints/[id]', 'DELETE'], ['/api/betas', 'POST'], ['/api/betas/[id]/promote', 'POST'], ['/api/betas/[id]/rollback', 'POST'], ['/api/campaigns/[id]/rollback', 'POST'], ['/api/deployments/[id]/rollback', 'POST'], ['/api/scripts', 'POST'], ['/api/scripts/[id]/versions', 'POST'], ['/api/scripts/[id]/versions/[versionId]', 'DELETE'], ['/api/configs', 'POST'], ['/api/plugins/github/import', 'POST'], ['/api/maintenance/[id]', 'POST'], ['/api/watchdog/[id]', 'POST'], ['/api/servers/[name]/probe-rcon', 'POST'] ])('requires an operator for %s %s', (route, method) => { expect(routePermission(route, method)).toBe('operator'); expect(allows('viewer', route, method)).toBe(false); expect(allows('operator', route, method)).toBe(true); expect(allows('admin', route, method)).toBe(true); }); it.each(['/api/users', '/api/users/[id]', '/api/users/[id]/password', '/settings', '/settings/users', '/server-keys', '/keys', '/api/server-keys'])('reserves all methods on %s for administrators', route => { for (const method of ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']) { expect(routePermission(route, method)).toBe('admin'); expect(allows('viewer', route, method)).toBe(false); expect(allows('operator', route, method)).toBe(false); expect(allows('admin', route, method)).toBe(true); } }); it.each([ ['/api/hosts', 'POST'], ['/api/hosts/[id]', 'PATCH'], ['/api/hosts/[id]', 'DELETE'], ['/api/hosts/[id]/enrollment', 'POST'], ['/api/hosts/[id]/credentials', 'DELETE'], ['/api/hosts/[id]/credentials', 'POST'], ['/api/hosts/[id]/operations', 'POST'], ['/api/settings', 'PUT'], ['/api/game-files', 'POST'], ['/api/game-files/[id]/download', 'POST'], ['/api/game-files/[id]/upload/files/[...path]', 'PUT'], ['/api/game-files/[id]/upload/complete', 'POST'], ['/api/maintenance', 'POST'], ['/api/watchdog', 'POST'], ['/api/plutonium', 'POST'] ])('protects host, game-file, and global policy mutations at %s', (route, method) => { expect(routePermission(route, method)).toBe('admin'); expect(allows('operator', route, method)).toBe(false); expect(allows('admin', route, method)).toBe(true); }); it.each([ '/configs/[id]', '/api/configs/[id]', '/api/configs/inspect', '/api/campaigns/[id]/file/[fileId]', '/api/deployments/[id]/file/[fileId]', '/api/servers/[name]/logs/[file]', '/api/servers/[name]/telemetry-requests' ])('prevents viewer access to secret-bearing raw content at %s', route => { for (const method of ['GET', 'HEAD']) { expect(routePermission(route, method)).toBe('operator'); expect(allows('viewer', route, method)).toBe(false); expect(allows('operator', route, method)).toBe(true); } }); }); describe('authentication and machine boundaries', () => { it('makes only leaderboard API reads public', () => { for (const method of ['GET', 'HEAD']) { expect(routePermission('/api/leaderboard', method)).toBe('public'); for (const route of ['/leaderboard', '/leaderboard/players/[guid]', '/leaderboard/players/[game]/[guid]', '/api/leaderboard/players/[guid]', '/api/leaderboard-extra', '/api/matches', '/api/servers/[name]/matches', '/matches/[id]']) { expect(routePermission(route, method)).toBe('viewer'); } } for (const method of ['POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']) { expect(routePermission('/api/leaderboard', method)).toBe('operator'); } }); it('makes only the intended login/bootstrap methods public', () => { for (const route of ['/login', '/setup']) { expect(routePermission(route, 'GET')).toBe('public'); expect(routePermission(route, 'HEAD')).toBe('public'); expect(routePermission(route, 'POST')).not.toBe('public'); } for (const route of ['/api/auth/login', '/api/auth/setup']) { expect(routePermission(route, 'POST')).toBe('public'); expect(routePermission(route, 'GET')).not.toBe('public'); expect(routePermission(route, 'DELETE')).not.toBe('public'); } expect(routePermission('/api/auth/unknown', 'POST')).toBe('admin'); }); it.each(['/account', '/api/auth/me', '/api/auth/password', '/api/auth/logout'])('permits every authenticated role to manage their own account at %s', route => { expect(routePermission(route, 'GET')).toBe('authenticated'); expect(routePermission(route, 'POST')).toBe('authenticated'); for (const role of roles) expect(allows(role, route, 'POST')).toBe(true); }); it.each([ ['/api/agent/enroll', 'POST'], ['/api/agent/heartbeat', 'POST'], ['/api/agent/operations', 'GET'], ['/api/agent/operations/[id]', 'POST'], ['/api/agent/libraries/[id]/files/[...path]', 'GET'], ['/api/agent/decommission/[id]', 'POST'], ['/api/agent/imports/[id]/files/[...path]', 'PUT'], ['/api/agent/imports/[id]/complete', 'POST'], ['/api/agent/install.sh', 'GET'], ['/api/agent/release/[file]', 'GET'], ['/api/telemetry', 'POST'] ])('preserves endpoint-specific machine authentication for %s', (route, method) => { expect(routePermission(route, method)).toBe('machine'); for (const role of roles) expect(allows(role, route, method)).toBe(false); }); it('does not grant machine exemptions to similar prefixes or unsupported telemetry methods', () => { for (const route of ['/api/agents', '/api/agentish/operations', '/api/telemetry-extra', '/api/telemetry/anything']) { expect(routePermission(route, 'POST')).toBe('operator'); } expect(routePermission('/api/telemetry', 'GET')).toBe('viewer'); expect(routePermission('/api/telemetry', 'PUT')).toBe('operator'); }); it('protects unknown routes and does not infer public assets from URL suffixes', () => { for (const route of [null, '/not-found', '/unrecognized.json', '/anything.css', '/__data.json']) { expect(routePermission(route, 'GET')).toBe('viewer'); expect(routePermission(route, 'POST')).toBe('operator'); expect(allows('viewer', route, 'POST')).toBe(false); } }); it.each(['/api/agent/../server-keys', '/api/%61gent/operations', '/api/agent/%2e%2e/settings', '/api/agent\\operations', '//api/agent/operations', '/api/agent/operations?bypass=1'])('fails closed if a caller supplies noncanonical route identity %s', route => { expect(routePermission(route, 'GET')).toBe('admin'); expect(routePermission(route, 'POST')).toBe('admin'); }); it('rejects invalid roles even when a permission would normally allow all users', () => { for (const permission of ['public', 'authenticated', 'viewer', 'operator', 'admin', 'machine'] as RoutePermission[]) { expect(roleAllows('unexpected' as UserRole, permission)).toBe(false); } }); });