This commit is contained in:
@@ -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
|
||||
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
|
||||
server's Diagnostics tab. Player addresses are redacted before diagnostics are
|
||||
saved; successful checks clear the saved reply. Fleet snapshots and live status
|
||||
|
||||
@@ -60,6 +60,9 @@ export function routePermission(routeId: string | null, method: string): RoutePe
|
||||
if (!reading && ['/api/maintenance', '/api/plutonium', '/api/watchdog'].includes(routeId))
|
||||
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
|
||||
// bytes, and unfiltered diagnostics may contain runtime credentials.
|
||||
if (
|
||||
|
||||
@@ -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 () => {
|
||||
signIn('admin', true);
|
||||
await expect(requestFor('/settings').invoke()).rejects.toMatchObject({
|
||||
|
||||
Reference in New Issue
Block a user