Add Super Viewer read-only administrator role
CI / checks (push) Successful in 5m40s

This commit is contained in:
2026-10-11 23:30:40 -04:00
parent 253d47300c
commit 4ea291d305
48 changed files with 1684 additions and 933 deletions
+48 -11
View File
@@ -234,10 +234,32 @@ describe('browser authentication guard', () => {
['operator', '/api/hosts/[id]', 'PATCH', 403],
['operator', '/api/users', 'GET', 403],
['admin', '/api/hosts/[id]', 'PATCH', 200],
['admin', '/api/users', 'GET', 200]
['admin', '/api/users', 'GET', 200],
['super_viewer', '/api/users', 'GET', 200],
['super_viewer', '/api/users/audit', 'GET', 200],
['super_viewer', '/api/server-keys', 'HEAD', 200],
['super_viewer', '/api/configs/[id]', 'GET', 200],
['super_viewer', '/api/servers/[name]/rcon-diagnostics', 'GET', 200],
['super_viewer', '/api/servers/[name]/logs/[file]', 'GET', 200],
['super_viewer', '/api/servers/[name]/telemetry-requests', 'GET', 200],
['super_viewer', '/api/users', 'POST', 403],
['super_viewer', '/api/users/[id]', 'PATCH', 403],
['super_viewer', '/api/users/[id]/revoke-sessions', 'POST', 403],
['super_viewer', '/api/server-keys', 'POST', 403],
['super_viewer', '/api/servers', 'POST', 403],
['super_viewer', '/api/provisioning/[id]/override', 'POST', 403],
['super_viewer', '/api/hosts/[id]', 'PATCH', 403],
['super_viewer', '/api/servers/[name]/probe-rcon', 'POST', 403],
['super_viewer', '/api/configs/inspect', 'POST', 403],
['super_viewer', '/api/settings/retention/preview', 'POST', 200],
['super_viewer', '/api/auth/password', 'POST', 200],
['super_viewer', '/api/auth/logout', 'POST', 200]
] as const)('enforces %s role for %s %s', async (role, routeId, method, expectedStatus) => {
const session = signIn(role);
const call = requestFor(routeId, { method, body: method === 'GET' ? undefined : {} });
const call = requestFor(routeId, {
method,
body: ['GET', 'HEAD'].includes(method) ? undefined : {}
});
const response = await call.invoke();
expect(response.status).toBe(expectedStatus);
if (expectedStatus === 200) {
@@ -245,14 +267,28 @@ describe('browser authentication guard', () => {
expect(call.event.locals.user).toEqual(session.user);
} else expect(call.resolve).not.toHaveBeenCalled();
});
it.each(['GET', 'HEAD'])(
'restricts global settings %s requests to administrators',
it.each(['/settings', '/settings/users', '/server-keys', '/configs/[id]'])(
'allows Super Viewer pages and page-data requests at %s',
async (route) => {
signIn('super_viewer');
for (const path of [route, `${route}/__data.json`]) {
const call = requestFor(route, { path });
expect((await call.invoke()).status).toBe(200);
expect(call.resolve).toHaveBeenCalledOnce();
}
}
);
it.each(['GET', 'HEAD', 'POST', 'PATCH', 'DELETE'])(
'permits only administrators to write settings and Super Viewers to read them',
async (method) => {
for (const role of ['viewer', 'operator', 'admin'] as const) {
for (const role of ['viewer', 'super_viewer', 'operator', 'admin'] as const) {
signIn(role);
const call = requestFor('/api/settings', { method });
expect((await call.invoke()).status).toBe(role === 'admin' ? 200 : 403);
if (role === 'admin') expect(call.resolve).toHaveBeenCalledOnce();
const allowed =
role === 'admin' ||
(role === 'super_viewer' && ['GET', 'HEAD'].includes(method));
expect((await call.invoke()).status).toBe(allowed ? 200 : 403);
if (allowed) expect(call.resolve).toHaveBeenCalledOnce();
else expect(call.resolve).not.toHaveBeenCalled();
}
}
@@ -260,7 +296,7 @@ describe('browser authentication guard', () => {
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) {
for (const role of ['viewer', 'super_viewer', 'operator', 'admin'] as const) {
signIn(role);
const call = requestFor('/api/betas/[id]/revisions/[revision]/files/[scriptId]', {
method,
@@ -282,11 +318,12 @@ describe('browser authentication guard', () => {
expect((await anonymous.invoke()).status).toBe(401);
expect(anonymous.resolve).not.toHaveBeenCalled();
for (const role of ['viewer', 'operator', 'admin'] as const) {
for (const role of ['viewer', 'super_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();
const allowed = role === 'operator' || role === 'admin';
expect((await call.invoke()).status).toBe(allowed ? 200 : 403);
if (!allowed) expect(call.resolve).not.toHaveBeenCalled();
else expect(call.resolve).toHaveBeenCalledOnce();
}
});