Restrict expensive viewer endpoints (Refs #72)
CI / checks (push) Successful in 4m45s

This commit is contained in:
2026-10-09 16:57:22 -04:00
parent 9eb3aa84c3
commit 15fb90200a
3 changed files with 26 additions and 0 deletions
+5
View File
@@ -47,6 +47,11 @@ cannot save keys indirectly through a launch request. An operator's content
permissions include executable scripts and plugins; assign that role only to permissions include executable scripts and plugins; assign that role only to
people trusted to operate the game fleet. people trusted to operate the game fleet.
Drift previews (`/api/drift`) and GitHub release lookups
(`/api/plugins/github/release`) require operator or administrator access, including
`GET` and `HEAD` requests. These endpoints initiate fleet file scans or use GitHub
API quota; viewers cannot trigger them.
RCON failure replies are available only to operators and administrators in the RCON failure replies are available only to operators and administrators in the
server's Diagnostics tab. Player addresses are redacted before diagnostics are server's Diagnostics tab. Player addresses are redacted before diagnostics are
saved; successful checks clear the saved reply. Fleet snapshots and live status saved; successful checks clear the saved reply. Fleet snapshots and live status
+3
View File
@@ -60,6 +60,9 @@ export function routePermission(routeId: string | null, method: string): RoutePe
if (!reading && ['/api/maintenance', '/api/plutonium', '/api/watchdog'].includes(routeId)) if (!reading && ['/api/maintenance', '/api/plutonium', '/api/watchdog'].includes(routeId))
return 'admin'; return 'admin';
// These reads initiate fleet file scans or spend external API quota.
if (routeId === '/api/drift' || routeId === '/api/plugins/github/release') return 'operator';
// Summaries remain visible to viewers; raw configuration, historical file // Summaries remain visible to viewers; raw configuration, historical file
// bytes, and unfiltered diagnostics may contain runtime credentials. // bytes, and unfiltered diagnostics may contain runtime credentials.
if ( if (
+18
View File
@@ -272,6 +272,24 @@ describe('browser authentication guard', () => {
} }
} }
); );
it.each([
['GET', '/api/drift'],
['HEAD', '/api/drift'],
['GET', '/api/plugins/github/release'],
['HEAD', '/api/plugins/github/release']
])('restricts expensive %s %s requests before handler execution', async (method, route) => {
const anonymous = requestFor(route, { method, cookie: null });
expect((await anonymous.invoke()).status).toBe(401);
expect(anonymous.resolve).not.toHaveBeenCalled();
for (const role of ['viewer', 'operator', 'admin'] as const) {
signIn(role);
const call = requestFor(route, { method });
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 () => { it('requires temporary-password users to change it even if their role is administrator', async () => {
signIn('admin', true); signIn('admin', true);
await expect(requestFor('/settings').invoke()).rejects.toMatchObject({ await expect(requestFor('/settings').invoke()).rejects.toMatchObject({