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:
1 parent
57e492cceb
commit
8bb1e01936
45 files changed
+1851
-190
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Checks the hub makes on what passes through it to a server (stage 5): which `settings/set`
|
||||
fields it will not forward, which values of `settings/get` a role may see, and which single
|
||||
identifier of a write may go into the audit log.
|
||||
|
||||
Pure functions, so the tests feed them any input.
|
||||
*/
|
||||
|
||||
/*
|
||||
`settings/set` and the hub's own way in.
|
||||
|
||||
The hub reaches a server through that server's web service, with an API token. Changing the
|
||||
service's bind addresses, ports, TLS or reverse-proxy settings from here can cut the hub off with
|
||||
no way back except the server's own console, so the hub refuses a change to any `webService*`
|
||||
field. The console sends the whole Web Service block with every save, unchanged values included,
|
||||
so a field is refused only when its value DIFFERS from what the server holds now (see
|
||||
`webServiceChanges`). Users, SSO and LDAP are not part of `settings/set`; a field that looks like
|
||||
one (`user*`, `sso*`, `ldap*`, `auth*`) is refused outright, in case a server version adds it.
|
||||
*/
|
||||
export const WEB_SERVICE_MESSAGE = "Change the web service settings in that server's own console."
|
||||
|
||||
const FORBIDDEN_FIELD = /^(user|sso|ldap|auth)/i
|
||||
const WEB_FIELD = /^webService/
|
||||
|
||||
/** True for a field the hub never forwards, whatever its value. */
|
||||
export const isForbiddenSettingsField = (name: string): boolean => FORBIDDEN_FIELD.test(name)
|
||||
|
||||
/* A list as the console sends it (`a,b`, one per line, or `false` for none) or as the server holds it. */
|
||||
function listOf(value: unknown): string[] {
|
||||
const items = Array.isArray(value) ? value.map(String) : String(value ?? '').split(/[,\n]/)
|
||||
return items.map((v) => v.trim()).filter((v) => v !== '' && v !== 'false')
|
||||
}
|
||||
|
||||
/* Whether a submitted text means what the server holds. Booleans and numbers by value, lists as sets, text trimmed. */
|
||||
function sameSetting(submitted: string, current: unknown): boolean {
|
||||
if (typeof current === 'boolean') return submitted.trim().toLowerCase() === String(current)
|
||||
if (typeof current === 'number') return submitted.trim() !== '' && Number(submitted) === current
|
||||
if (Array.isArray(current)) {
|
||||
const a = listOf(submitted).sort()
|
||||
const b = listOf(current).sort()
|
||||
return a.length === b.length && a.every((v, i) => v === b[i])
|
||||
}
|
||||
return submitted.trim() === String(current ?? '').trim()
|
||||
}
|
||||
|
||||
/*
|
||||
The `webService*` fields of a `settings/set` whose value differs from the server's current
|
||||
settings (`settings/get`). A field the server does not report is not compared: it would be ignored
|
||||
there too.
|
||||
*/
|
||||
export function webServiceChanges(submitted: Record<string, string>, current: unknown): string[] {
|
||||
const now = (current !== null && typeof current === 'object' ? current : {}) as Record<string, unknown>
|
||||
const changed: string[] = []
|
||||
for (const [name, value] of Object.entries(submitted)) {
|
||||
if (!WEB_FIELD.test(name) || !Object.hasOwn(now, name)) continue
|
||||
if (!sameSetting(value, now[name])) changed.push(name)
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
/*
|
||||
`settings/get` carries secrets: the TSIG shared secrets, the forward proxy password and the two
|
||||
certificate passwords. A role without `admin` gets a copy with those values blanked (a viewer
|
||||
needs the screen, not the keys); an admin gets them, since saving the form sends them back. The
|
||||
blanked copy is for display only and `settings/set` is `admin`-only, so a blank cannot be saved
|
||||
over a real secret by someone who may not read it.
|
||||
*/
|
||||
export function redactSettings(response: unknown): unknown {
|
||||
if (response === null || typeof response !== 'object') return response
|
||||
const copy = { ...(response as Record<string, unknown>) }
|
||||
for (const key of ['webServiceTlsCertificatePassword', 'dnsTlsCertificatePassword']) {
|
||||
if (typeof copy[key] === 'string' && copy[key] !== '') copy[key] = ''
|
||||
}
|
||||
if (Array.isArray(copy.tsigKeys)) {
|
||||
copy.tsigKeys = copy.tsigKeys.map((k: unknown) => (k !== null && typeof k === 'object' ? { ...k, sharedSecret: '' } : k))
|
||||
}
|
||||
const proxy = copy.proxy
|
||||
if (proxy !== null && typeof proxy === 'object' && typeof (proxy as { password?: unknown }).password === 'string') {
|
||||
copy.proxy = { ...proxy, password: '' }
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
/*
|
||||
The one identifier an audit line may carry for a write: a zone, a domain, a name or a scope. Never
|
||||
the other parameters (records carry data, settings carry passwords and TSIG secrets). Cut short.
|
||||
*/
|
||||
const IDENTIFIERS = ['zone', 'domain', 'name', 'scope', 'zones']
|
||||
|
||||
export function safeTarget(params: Record<string, string>): string | undefined {
|
||||
for (const key of IDENTIFIERS) {
|
||||
const value = params[key]
|
||||
if (typeof value === 'string' && value.trim() !== '') return value.trim().slice(0, 120)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
Reference in new issue
Block a user