pediatric-ai-scribe-v3/src/utils/adminLockdown.js
Daniel 272ea94768 feat: admin lockdown, so several admins do not all get to change everything
With more than one admin, every setting in the panel was editable by all of
them — prompts, model policy, retrieval budgets, SMTP, email templates.
ADMIN_LOCKDOWN separates running the service from changing how it behaves.

It is an environment variable on purpose: a setting could be switched off by
the very admin it restrains, so lifting this needs host access and a restart.

The server is the control. One gate refuses configuration writes rather than
a check in each of the fifteen write routes, because that list grows and a
route added later would quietly miss it. Reads always pass — lockdown hides
nothing. Day-to-day operation stays available: invitations, announcements,
registration, feature flags, and the test endpoints, which persist nothing.
A setting invented later is locked until someone deliberately makes it
editable, rather than defaulting to open.

The panel disables what it cannot save and says why, but that is courtesy;
the refusal is what enforces it.

Two things this taught me, both fixed: my first version painted the panel
from an IIFE, which the module conventions forbid, and fetched the whole
config a second time just to read one flag — breaking the test that pins
admin loaders firing exactly once. The state now rides on the invites
response the panel already requests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-10 23:20:29 +02:00

79 lines
2.9 KiB
JavaScript

// ============================================================
// ADMIN LOCKDOWN
// With several admins, everything in the admin panel is editable by all of
// them — prompts, model policy, retrieval limits, SMTP. Lockdown separates
// "runs the service day to day" from "changes how the service behaves".
//
// It is an environment variable, deliberately, not a setting. A setting could
// be switched off by any admin, which would make it decoration. Only someone
// with access to the host and a restart can lift it.
//
// Locked settings stay visible — an admin should be able to see how the system
// is configured — but the field is read-only and the server refuses the write.
// The refusal is the real control; the read-only field is only courtesy.
// ============================================================
// Everything that changes how the assistant answers, what it costs, or how mail
// leaves the building.
var LOCKED_PREFIXES = Object.freeze([
'prompt.', // every prompt, including the clinical ones
'clinical_assistant.', // models, allowlists, retrieval limits, budgets
'models.', // model policy: default, custom, enabled set
'tts.',
'stt.',
'embeddings.',
'smtp.', // where mail goes and who it authenticates as
'email.' // the templates that mail sends
]);
// Day-to-day operation stays with ordinary admins.
var EDITABLE_WHEN_LOCKED = Object.freeze([
'announcement.',
'registration_enabled',
'registration_invite_only',
'feature.',
'site.'
]);
function enabled(env) {
var raw = (env || process.env).ADMIN_LOCKDOWN;
return String(raw == null ? '' : raw).toLowerCase() === 'true';
}
// A key is locked when lockdown is on and it is not on the day-to-day list.
// Anything unrecognised is locked: a setting added later should need a
// deliberate decision to become editable, rather than defaulting to open.
function isLocked(key, env) {
if (!enabled(env)) return false;
var name = String(key || '');
for (var i = 0; i < EDITABLE_WHEN_LOCKED.length; i++) {
var allowed = EDITABLE_WHEN_LOCKED[i];
if (name === allowed || (allowed.endsWith('.') && name.startsWith(allowed))) return false;
}
return true;
}
// What the admin UI needs to render itself correctly.
function state(env) {
return {
enabled: enabled(env),
lockedPrefixes: LOCKED_PREFIXES.slice(),
editablePrefixes: EDITABLE_WHEN_LOCKED.slice(),
reason: 'ADMIN_LOCKDOWN is set on the server. Locked settings can only be changed by someone with host access.'
};
}
// One message, so every refusal reads the same wherever it comes from.
function refusal(key) {
return 'This setting is locked. The server is in admin lockdown (ADMIN_LOCKDOWN), ' +
'so "' + key + '" can only be changed by someone with host access.';
}
module.exports = {
LOCKED_PREFIXES,
EDITABLE_WHEN_LOCKED,
enabled,
isLocked,
state,
refusal
};