Hub stage 5: manage each server from one console

Zones, Cache, Blocking, DNS Client, Settings, DHCP and Logs can now be
changed on one chosen server through the hub, with that server's admin
token. Operators manage cache and blocking; admins also zones, settings
and DHCP. Under "All servers", or on a server without an admin token,
the controls are disabled with a hint.

The hub refuses settings/set changes to a server's web service fields
and anything about users, SSO or LDAP; backup, restore, log and stats
deletion and admin/* stay refused. Non-admins get settings with secrets
blanked. Write params travel in the POST body, downloads pass through
with a single-use token, and the audit log names no values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-31 22:06:37 +07:00
1 parent 57e492cceb
commit 8bb1e01936
45 files changed
+1851 -190

No files matched your search

+4 -4
View File
@@ -286,7 +286,7 @@ describe('audit', () => {
expect(adminLogin.body.hubRole).toBe('admin')
const viewer = (await login(base, 'vic', 'password2')).body.token as string
expect((await call(base, 'zones/delete', { zone: 'x', node: 'a' }, admin)).code).toBe(403)
expect((await call(base, 'admin/users/delete', { user: 'x', node: 'a' }, admin)).code).toBe(403)
expect((await call(base, 'hub/audit/list', {}, viewer)).code).toBe(403)
await call(base, 'user/logout', {}, viewer)
@@ -296,7 +296,7 @@ describe('audit', () => {
const actions = entries.map((e) => `${e.action}:${e.outcome}`)
expect(actions).toEqual(expect.arrayContaining(['login:failed', 'login:ok', 'refused-write:refused', 'denied-read:refused', 'logout:ok']))
const refused = entries.find((e) => e.action === 'refused-write')
expect(refused).toMatchObject({ user: 'alice', role: 'admin', target: 'zones/delete', server: 'a' })
expect(refused).toMatchObject({ user: 'alice', role: 'admin', target: 'admin/users/delete', server: 'a' })
const ts = entries.map((e) => e.ts)
expect([...ts].sort().reverse()).toEqual(ts)
@@ -319,11 +319,11 @@ describe('audit', () => {
describe('routes table', () => {
it('lists every old read endpoint as a read and hub/audit/list as audit.read', () => {
for (const p of [
'dashboard/stats/get', 'dashboard/stats/getTop', 'dashboard/metrics/json', 'apps/list', 'logs/query', 'logs/list',
'dashboard/stats/get', 'dashboard/stats/getTop', 'dashboard/metrics/json', 'apps/list', 'logs/query',
'settings/get', 'user/checkForUpdate', 'admin/cluster/state',
]) expect(ROUTES[p]).toEqual({ kind: 'read', capability: 'read' })
expect(routeFor('hub/audit/list')).toEqual({ kind: 'read', capability: 'audit.read' })
expect(routeFor('zones/delete')).toBeUndefined()
expect(routeFor('logs/delete')).toBeUndefined()
expect(routeFor('constructor')).toBeUndefined()
expect(routeFor('hub/nothing')).toBeUndefined()
})