mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
remove useless slop stuff
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Configuration System Tests
|
||||
// Purpose: Verifies strict defaults, immutable revisions, secret handling, legacy import, and administrator safety.
|
||||
// Purpose: Verifies strict defaults, immutable revisions, secret handling, explicit setup-file import, and administrator safety.
|
||||
// Scope: Uses isolated temporary databases and never opens the development server's data store.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
@@ -10,7 +10,7 @@ const { defaultConfig, normalizeConfig, assertValidConfig } = require('./validat
|
||||
const { definitions, rootSchema, secretPaths, featureDefinitions } = require('./definition');
|
||||
const { getFeatureFlags } = require('./index');
|
||||
const { createConfigurationDatabase } = require('./database');
|
||||
const { parseLegacyConfiguration, importLegacyConfiguration } = require('./legacyImporter');
|
||||
const { parseConfigurationFile, importConfigurationFile } = require('./configurationFileImporter');
|
||||
|
||||
const temporaryRoots = [];
|
||||
|
||||
@@ -168,7 +168,7 @@ test('administrator storage never exposes hashes or removes the final lockdown a
|
||||
database.close();
|
||||
});
|
||||
|
||||
test('legacy YAML imports configuration and bcrypt hashes exactly once', () => {
|
||||
test('an explicitly uploaded YAML file imports configuration and bcrypt hashes exactly once', () => {
|
||||
const yamlText = `
|
||||
admins:
|
||||
- username: owner
|
||||
@@ -179,14 +179,14 @@ timezone: America/Chicago
|
||||
media:
|
||||
whepBaseUrl: http://localhost:8889/video
|
||||
`;
|
||||
const parsed = parseLegacyConfiguration(yamlText);
|
||||
const parsed = parseConfigurationFile(yamlText);
|
||||
assert.equal(parsed.config.timezone, 'America/Chicago');
|
||||
assert.equal(parsed.administrators[0].passwordHash, '$2b$10$preservedHash');
|
||||
|
||||
const database = createTestDatabase();
|
||||
const result = importLegacyConfiguration({ text: yamlText, database, dryRun: false });
|
||||
const result = importConfigurationFile({ text: yamlText, database });
|
||||
assert.equal(result.administratorCount, 1);
|
||||
assert.equal(database.findAdministratorForAuthentication('OWNER').passwordHash, '$2b$10$preservedHash');
|
||||
assert.throws(() => importLegacyConfiguration({ text: yamlText, database }), /cannot replace an initialized installation/);
|
||||
assert.throws(() => importConfigurationFile({ text: yamlText, database }), /cannot replace an initialized installation/);
|
||||
database.close();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Configuration File Importer
|
||||
// Purpose: Validates one YAML file deliberately uploaded during first-run setup and stores it in the configuration database.
|
||||
// Scope: This is an explicit setup action only; startup and installation never search for or consume configuration files.
|
||||
const yaml = require('js-yaml');
|
||||
const { normalizeConfig, assertValidConfig } = require('./validation');
|
||||
|
||||
function normalizeUploadedAdministrator(entry, index) {
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||
throw new Error(`Administrator ${index + 1} must be an object.`);
|
||||
}
|
||||
const username = String(entry.username || '').trim();
|
||||
const passwordHash = String(entry.password_hash || '').trim();
|
||||
if (!username || !passwordHash) {
|
||||
throw new Error(`Administrator ${index + 1} requires username and password_hash.`);
|
||||
}
|
||||
return {
|
||||
username,
|
||||
passwordHash,
|
||||
discordId: String(entry.discord_id || '').trim(),
|
||||
role: entry.lockdown ? 'lockdown' : 'admin',
|
||||
};
|
||||
}
|
||||
|
||||
function parseConfigurationFile(text) {
|
||||
const parsed = yaml.load(String(text || ''));
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('The configuration file must contain a YAML object.');
|
||||
}
|
||||
|
||||
const administrators = Array.isArray(parsed.admins)
|
||||
? parsed.admins.map(normalizeUploadedAdministrator)
|
||||
: [];
|
||||
const configInput = Object.fromEntries(
|
||||
Object.entries(parsed).filter(([key]) => key !== 'admins'),
|
||||
);
|
||||
const config = normalizeConfig(configInput);
|
||||
|
||||
/*
|
||||
Unknown fields remain in the normalized document so strict schema
|
||||
validation reports them to the operator instead of silently losing data
|
||||
from the explicitly selected file.
|
||||
*/
|
||||
assertValidConfig(config);
|
||||
if (!administrators.some((admin) => admin.role === 'lockdown')) {
|
||||
throw new Error('The configuration file must contain at least one lockdown administrator.');
|
||||
}
|
||||
return { config, administrators };
|
||||
}
|
||||
|
||||
function importConfigurationFile({ text, database, actor = 'setup-file-upload', source = 'uploaded-config.yaml' }) {
|
||||
const result = parseConfigurationFile(text);
|
||||
const revision = database.importConfigurationFile({
|
||||
config: result.config,
|
||||
administrators: result.administrators,
|
||||
actor,
|
||||
source,
|
||||
});
|
||||
return {
|
||||
revision,
|
||||
administratorCount: result.administrators.length,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseConfigurationFile,
|
||||
importConfigurationFile,
|
||||
};
|
||||
@@ -331,8 +331,10 @@ function createConfigurationDatabase({ databasePath = DEFAULT_DATABASE_PATH } =
|
||||
}));
|
||||
}
|
||||
|
||||
const importLegacyTransaction = db.transaction(({ config, administrators, actor, source }) => {
|
||||
if (isSetupComplete()) throw new Error('Legacy configuration cannot replace an initialized installation.');
|
||||
const importConfigurationFileTransaction = db.transaction(({ config, administrators, actor, source }) => {
|
||||
// A setup upload initializes an empty installation; it is deliberately not
|
||||
// a general-purpose replacement path for a running server's configuration.
|
||||
if (isSetupComplete()) throw new Error('A configuration file cannot replace an initialized installation.');
|
||||
const normalized = assertValidConfig(normalizeConfig(config));
|
||||
const revision = commitRevisionTransaction(normalized, {
|
||||
expectedRevision: getActiveConfigurationRecord().revision,
|
||||
@@ -340,13 +342,13 @@ function createConfigurationDatabase({ databasePath = DEFAULT_DATABASE_PATH } =
|
||||
source,
|
||||
});
|
||||
administrators.forEach((admin) => createAdministratorTransaction(admin, actor, false));
|
||||
if (!isSetupComplete()) throw new Error('Legacy import must contain at least one lockdown administrator.');
|
||||
writeAudit(actor, 'legacy-import.completed', { revision, administratorCount: administrators.length, source });
|
||||
if (!isSetupComplete()) throw new Error('The configuration file must contain at least one lockdown administrator.');
|
||||
writeAudit(actor, 'setup.configuration-file-imported', { revision, administratorCount: administrators.length, source });
|
||||
return revision;
|
||||
});
|
||||
|
||||
function importLegacy(payload) {
|
||||
return importLegacyTransaction(payload);
|
||||
function importConfigurationFile(payload) {
|
||||
return importConfigurationFileTransaction(payload);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -364,7 +366,7 @@ function createConfigurationDatabase({ databasePath = DEFAULT_DATABASE_PATH } =
|
||||
countLockdownAdministrators,
|
||||
isSetupComplete,
|
||||
listAuditEvents,
|
||||
importLegacy,
|
||||
importConfigurationFile,
|
||||
close: () => db.close(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// Legacy YAML Configuration Importer
|
||||
// Purpose: Converts one explicitly supplied config.yaml document into the database-backed configuration model.
|
||||
// Scope: Parses and validates legacy input without becoming a runtime fallback or watcher.
|
||||
const yaml = require('js-yaml');
|
||||
const { normalizeConfig, assertValidConfig } = require('./validation');
|
||||
|
||||
function normalizeLegacyAdministrator(entry, index) {
|
||||
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
||||
throw new Error(`Legacy administrator ${index + 1} must be an object.`);
|
||||
}
|
||||
const username = String(entry.username || '').trim();
|
||||
const passwordHash = String(entry.password_hash || '').trim();
|
||||
if (!username || !passwordHash) {
|
||||
throw new Error(`Legacy administrator ${index + 1} requires username and password_hash.`);
|
||||
}
|
||||
return {
|
||||
username,
|
||||
passwordHash,
|
||||
discordId: String(entry.discord_id || '').trim(),
|
||||
role: entry.lockdown ? 'lockdown' : 'admin',
|
||||
};
|
||||
}
|
||||
|
||||
function parseLegacyConfiguration(text) {
|
||||
const parsed = yaml.load(String(text || ''));
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('Legacy configuration must contain a YAML object.');
|
||||
}
|
||||
|
||||
const administrators = Array.isArray(parsed.admins)
|
||||
? parsed.admins.map(normalizeLegacyAdministrator)
|
||||
: [];
|
||||
const configInput = Object.fromEntries(
|
||||
Object.entries(parsed).filter(([key]) => key !== 'admins'),
|
||||
);
|
||||
const config = normalizeConfig(configInput);
|
||||
|
||||
/*
|
||||
Unknown fields are retained by normalizeConfig and therefore appear in
|
||||
Ajv's precise validation errors instead of being silently discarded during
|
||||
the one-time import.
|
||||
*/
|
||||
assertValidConfig(config);
|
||||
if (!administrators.some((admin) => admin.role === 'lockdown')) {
|
||||
throw new Error('Legacy configuration must contain at least one lockdown administrator.');
|
||||
}
|
||||
return { config, administrators };
|
||||
}
|
||||
|
||||
function importLegacyConfiguration({ text, database, actor = 'legacy-import', source = 'config.yaml', dryRun = false }) {
|
||||
const result = parseLegacyConfiguration(text);
|
||||
if (dryRun) {
|
||||
return {
|
||||
dryRun: true,
|
||||
administratorCount: result.administrators.length,
|
||||
};
|
||||
}
|
||||
const revision = database.importLegacy({
|
||||
config: result.config,
|
||||
administrators: result.administrators,
|
||||
actor,
|
||||
source,
|
||||
});
|
||||
return {
|
||||
dryRun: false,
|
||||
revision,
|
||||
administratorCount: result.administrators.length,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseLegacyConfiguration,
|
||||
importLegacyConfiguration,
|
||||
};
|
||||
@@ -1,34 +1,38 @@
|
||||
// First-Run Setup Service
|
||||
// Purpose: Allows an empty data directory to create its first lockdown administrator or import legacy YAML safely.
|
||||
// Purpose: Allows an empty data directory to create its first lockdown administrator or import an explicitly uploaded YAML file.
|
||||
// Scope: Exposes setup-only socket operations and permanently closes them once a lockdown administrator exists.
|
||||
const crypto = require('crypto');
|
||||
const bcrypt = require('bcrypt');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('setupService');
|
||||
const { getConfigurationDatabase } = require('../../configuration');
|
||||
const { importLegacyConfiguration } = require('../../configuration/legacyImporter');
|
||||
const { importConfigurationFile } = require('../../configuration/configurationFileImporter');
|
||||
const { createSetupCodeFile } = require('./setupCodeFile');
|
||||
|
||||
const MAX_LEGACY_YAML_BYTES = 1024 * 1024;
|
||||
const MAX_CONFIGURATION_FILE_BYTES = 1024 * 1024;
|
||||
const database = getConfigurationDatabase();
|
||||
let setupCode = null;
|
||||
const setupCodeFile = createSetupCodeFile();
|
||||
let setupNoticeLogged = false;
|
||||
|
||||
function isSetupRequired() {
|
||||
return !database.isSetupComplete();
|
||||
}
|
||||
|
||||
function ensureSetupCode() {
|
||||
if (!isSetupRequired()) return null;
|
||||
if (!setupCode) {
|
||||
setupCode = crypto.randomBytes(6).toString('hex');
|
||||
/*
|
||||
The code is intentionally logged only on a server that has no lockdown
|
||||
administrator. It lives in process memory, changes on restart, and is
|
||||
permanently irrelevant as soon as setup succeeds, so it cannot become a
|
||||
recurring environment-variable authentication bypass.
|
||||
*/
|
||||
logger.warn('First-run setup is required', { setupCode });
|
||||
if (!isSetupRequired()) {
|
||||
// Setup authorization permanently closes when the first lockdown account
|
||||
// exists. Remove a stale credential left by an interrupted final response.
|
||||
setupCodeFile.remove();
|
||||
return null;
|
||||
}
|
||||
return setupCode;
|
||||
const code = setupCodeFile.ensure();
|
||||
// Logs may be retained or shipped elsewhere, so they identify the local file
|
||||
// containing the credential without ever including the credential itself.
|
||||
if (!setupNoticeLogged) {
|
||||
logger.warn('First-run setup is required', { setupCodePath: setupCodeFile.filePath });
|
||||
setupNoticeLogged = true;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function requireOpenSetup(candidateCode) {
|
||||
@@ -73,25 +77,25 @@ io.on('connection', (socket) => {
|
||||
discordId: payload.discordId,
|
||||
role: 'lockdown',
|
||||
}, 'first-run-setup');
|
||||
setupCode = null;
|
||||
setupCodeFile.remove();
|
||||
return { administrator };
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('setup:importLegacy', (payload = {}, cb = () => {}) => {
|
||||
socket.on('setup:importConfigurationFile', (payload = {}, cb = () => {}) => {
|
||||
respond(cb, () => {
|
||||
requireOpenSetup(payload.setupCode);
|
||||
const yamlText = String(payload.yaml || '');
|
||||
if (!yamlText || Buffer.byteLength(yamlText, 'utf8') > MAX_LEGACY_YAML_BYTES) {
|
||||
throw new Error('Legacy YAML must be present and no larger than 1 MiB.');
|
||||
if (!yamlText || Buffer.byteLength(yamlText, 'utf8') > MAX_CONFIGURATION_FILE_BYTES) {
|
||||
throw new Error('The YAML configuration file must be present and no larger than 1 MiB.');
|
||||
}
|
||||
const result = importLegacyConfiguration({
|
||||
const result = importConfigurationFile({
|
||||
text: yamlText,
|
||||
database,
|
||||
actor: 'first-run-setup',
|
||||
source: String(payload.fileName || 'uploaded-config.yaml').slice(0, 255),
|
||||
});
|
||||
setupCode = null;
|
||||
setupCodeFile.remove();
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Setup Code File
|
||||
// Purpose: Persists the one-time first-run credential inside the server data directory.
|
||||
// Scope: Owns secure file creation, validation, reuse, and removal without knowing whether setup is complete.
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { resolveDataPath } = require('../../helpers/dataPaths');
|
||||
|
||||
const DEFAULT_SETUP_CODE_PATH = resolveDataPath('setup-code.txt');
|
||||
const SETUP_CODE_PATTERN = /^[0-9a-f]{12}$/;
|
||||
|
||||
function createSetupCodeFile({ filePath = DEFAULT_SETUP_CODE_PATH } = {}) {
|
||||
function read() {
|
||||
const fileStats = fs.lstatSync(filePath);
|
||||
if (!fileStats.isFile()) {
|
||||
// In particular, reject symbolic links before chmod or read operations so
|
||||
// a writable data directory cannot redirect setup handling to another file.
|
||||
throw new Error(`Setup code path is not a regular file: ${filePath}`);
|
||||
}
|
||||
fs.chmodSync(filePath, 0o600);
|
||||
const code = fs.readFileSync(filePath, 'utf8').trim().toLowerCase();
|
||||
if (!SETUP_CODE_PATTERN.test(code)) {
|
||||
/*
|
||||
Never silently replace a malformed credential. An operator may already
|
||||
be reading that file, and changing it behind their back would make setup
|
||||
failures mysterious while concealing possible filesystem corruption.
|
||||
*/
|
||||
throw new Error(`Setup code file is invalid: ${filePath}`);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
function ensure() {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
if (fs.existsSync(filePath)) {
|
||||
return read();
|
||||
}
|
||||
|
||||
const code = crypto.randomBytes(6).toString('hex');
|
||||
try {
|
||||
/*
|
||||
Exclusive creation prevents two accidentally overlapping server starts
|
||||
from overwriting one another's setup credential. The file is the source
|
||||
an operator reads, so the accepted code must always match its contents.
|
||||
*/
|
||||
fs.writeFileSync(filePath, `${code}\n`, {
|
||||
encoding: 'utf8',
|
||||
flag: 'wx',
|
||||
mode: 0o600,
|
||||
});
|
||||
return code;
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') throw error;
|
||||
return read();
|
||||
}
|
||||
}
|
||||
|
||||
function remove() {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
}
|
||||
|
||||
return {
|
||||
filePath,
|
||||
ensure,
|
||||
read,
|
||||
remove,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_SETUP_CODE_PATH,
|
||||
SETUP_CODE_PATTERN,
|
||||
createSetupCodeFile,
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
// Setup Code File Tests
|
||||
// Purpose: Verifies the first-run credential remains private, stable, and removable.
|
||||
// Scope: Uses an isolated operating-system temporary directory and never touches development server data.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const test = require('node:test');
|
||||
|
||||
const { SETUP_CODE_PATTERN, createSetupCodeFile } = require('./setupCodeFile');
|
||||
|
||||
const temporaryRoots = [];
|
||||
|
||||
function createTestStore() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'multirover-setup-code-'));
|
||||
temporaryRoots.push(root);
|
||||
return createSetupCodeFile({ filePath: path.join(root, 'setup-code.txt') });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
temporaryRoots.forEach((root) => fs.rmSync(root, { recursive: true, force: true }));
|
||||
});
|
||||
|
||||
test('creates one owner-readable code and reuses it across startup initialization', () => {
|
||||
const store = createTestStore();
|
||||
const firstCode = store.ensure();
|
||||
const secondCode = store.ensure();
|
||||
|
||||
assert.match(firstCode, SETUP_CODE_PATTERN);
|
||||
assert.equal(secondCode, firstCode);
|
||||
assert.equal(fs.readFileSync(store.filePath, 'utf8'), `${firstCode}\n`);
|
||||
// Mask off file-type bits so this assertion checks only Unix permissions.
|
||||
assert.equal(fs.statSync(store.filePath).mode & 0o777, 0o600);
|
||||
});
|
||||
|
||||
test('rejects a malformed existing credential instead of replacing it', () => {
|
||||
const store = createTestStore();
|
||||
fs.writeFileSync(store.filePath, 'not-a-valid-code\n', { mode: 0o600 });
|
||||
|
||||
assert.throws(() => store.ensure(), /Setup code file is invalid/);
|
||||
assert.equal(fs.readFileSync(store.filePath, 'utf8'), 'not-a-valid-code\n');
|
||||
});
|
||||
|
||||
test('rejects a setup-code symlink without reading or changing its target', () => {
|
||||
const store = createTestStore();
|
||||
const targetPath = path.join(path.dirname(store.filePath), 'unrelated.txt');
|
||||
fs.writeFileSync(targetPath, 'unrelated-content\n', { mode: 0o644 });
|
||||
fs.symlinkSync(targetPath, store.filePath);
|
||||
|
||||
assert.throws(() => store.ensure(), /not a regular file/);
|
||||
assert.equal(fs.readFileSync(targetPath, 'utf8'), 'unrelated-content\n');
|
||||
assert.equal(fs.statSync(targetPath).mode & 0o777, 0o644);
|
||||
});
|
||||
|
||||
test('removes the credential after setup completes', () => {
|
||||
const store = createTestStore();
|
||||
store.ensure();
|
||||
store.remove();
|
||||
|
||||
assert.equal(fs.existsSync(store.filePath), false);
|
||||
});
|
||||
Reference in New Issue
Block a user