diff --git a/src/lib/server/auth/access.ts b/src/lib/server/auth/access.ts index 7396fe9..1dcf3bb 100644 --- a/src/lib/server/auth/access.ts +++ b/src/lib/server/auth/access.ts @@ -39,6 +39,7 @@ export function routePermission(routeId: string | null, method: string): RoutePe // 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/betas/[id]/revisions/[revision]/files/[scriptId]' || routeId === '/api/servers/[name]/logs/[file]' || routeId === '/api/servers/[name]/telemetry-requests') return 'operator'; return reading ? 'viewer' : 'operator'; diff --git a/tests/unit/auth-access.test.ts b/tests/unit/auth-access.test.ts index 110568c..bce6ba6 100644 --- a/tests/unit/auth-access.test.ts +++ b/tests/unit/auth-access.test.ts @@ -61,6 +61,7 @@ describe('management route permissions', () => { it.each([ '/configs/[id]', '/api/configs/[id]', '/api/configs/inspect', '/api/campaigns/[id]/file/[fileId]', '/api/deployments/[id]/file/[fileId]', + '/api/betas/[id]/revisions/[revision]/files/[scriptId]', '/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']) { diff --git a/tests/unit/auth-guard.test.ts b/tests/unit/auth-guard.test.ts index e8f2c41..3c81b1a 100644 --- a/tests/unit/auth-guard.test.ts +++ b/tests/unit/auth-guard.test.ts @@ -125,6 +125,17 @@ describe('browser authentication guard', () => { if (expectedStatus === 200) { expect(call.resolve).toHaveBeenCalledOnce(); expect(call.event.locals.user).toEqual(session.user); } else expect(call.resolve).not.toHaveBeenCalled(); }); + it.each(['GET', 'HEAD'])('restricts raw beta file %s requests to operators and administrators', async method => { + for (const role of ['viewer', 'operator', 'admin'] as const) { + signIn(role); + const call = requestFor('/api/betas/[id]/revisions/[revision]/files/[scriptId]', { + method, path: '/api/betas/1/revisions/1/files/1' + }); + expect((await call.invoke()).status).toBe(role === 'viewer' ? 403 : 200); + if (role === 'viewer') expect(call.resolve).not.toHaveBeenCalled(); + else expect(call.resolve).toHaveBeenCalledOnce(); + } + }); it('requires temporary-password users to change it even if their role is administrator', async () => { signIn('admin', true); await expect(requestFor('/settings').invoke()).rejects.toMatchObject({ status: 303, location: '/account' });