Files
multi-simadmin/apps/api/src/application/legacy-import/activate-pending-secret.test.ts
T
chick e4c4bce75b feat(secrets): add a 0600 file secret backend so production runs on Linux
The production composition hardwired the macOS Keychain (/usr/bin/security),
which made Linux deployment impossible. Now:

- SecretStore gains a stable provider identity persisted in
  secret_references.provider; services stop hardcoding 'macos-keychain'
- shared reference codec (secret-reference.ts) understands both
  keychain:// and secret-file:// references
- FileSecretStore: single 0600 JSON map under the data root, atomic
  temp+rename writes, serialized in-process, same validation envelope
- production composition picks the backend via
  MULTI_SIMADMIN_SECRET_BACKEND (default: Keychain on darwin, file store
  elsewhere) and readiness probes the matching backend
2026-09-07 01:32:07 +08:00

274 lines
10 KiB
TypeScript

import Database from 'better-sqlite3';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { openDatabase } from '../../infrastructure/database/database.js';
import { migrateDatabase } from '../../infrastructure/database/migrations.js';
import type { SecretKey, SecretStore } from '../../infrastructure/secrets/secret-store.js';
import { ActivationError, activatePendingSecret } from './activate-pending-secret.js';
const secret = 'activation-secret-never-in-sqlite';
const databases: Database.Database[] = [];
afterEach(() => {
for (const db of databases.splice(0)) db.close();
});
function pendingDatabase(): Database.Database {
const db = new Database(':memory:');
db.pragma('foreign_keys = ON');
migrateDatabase(db);
const now = '2026-07-16T00:00:00.000Z';
db.prepare(
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
).run('alpha', 'Alpha', 'https://203.0.113.8', 'none', 1, 3, now, now);
db.prepare('INSERT INTO app_settings VALUES (?,?,?,?)').run(
'legacy-import.instance.alpha',
JSON.stringify({
source: 'legacy-config',
secretPending: true,
requestedAuthMode: 'password',
secretPurpose: 'instance-password',
}),
now,
now,
);
databases.push(db);
return db;
}
class FakeStore implements SecretStore {
readonly provider = 'macos-keychain';
readonly sets: { key: SecretKey; value: string }[] = [];
readonly deletes: string[] = [];
setError?: Error;
deleteError?: Error;
reference = 'keychain://multi-simadmin/b3BhcXVl';
async set(key: SecretKey, value: string): Promise<string> {
this.sets.push({ key, value });
if (this.setError) throw this.setError;
return key.slot ? `${this.reference}-${key.slot}` : this.reference;
}
async get(): Promise<string | undefined> {
return undefined;
}
async delete(reference: string): Promise<boolean> {
this.deletes.push(reference);
if (this.deleteError) throw this.deleteError;
return true;
}
}
function allText(db: Database.Database): string {
const tables = db
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
.all() as { name: string }[];
const rows: unknown[] = [];
for (const { name } of tables) {
const columns = (
db.prepare(`PRAGMA table_info("${name}")`).all() as { name: string; type: string }[]
)
.filter(({ type }) => type.toUpperCase().includes('TEXT'))
.map(({ name: column }) => `"${column}"`);
if (columns.length)
rows.push(...db.prepare(`SELECT ${columns.join(',')} FROM "${name}"`).all());
}
return JSON.stringify(rows);
}
function state(db: Database.Database): Uint8Array {
return db.serialize();
}
describe('activatePendingSecret', () => {
it('writes keychain first then atomically activates password auth and clears pending metadata', async () => {
const db = pendingDatabase();
const store = new FakeStore();
const result = await activatePendingSecret({ db, store, instanceId: 'alpha', secret });
expect(store.sets).toHaveLength(1);
expect(store.sets[0]).toEqual({
key: {
instanceId: 'alpha',
purpose: 'instance-password',
slot: expect.stringMatching(/^[A-Za-z0-9_-]+$/),
},
value: secret,
});
const storedReference = `${store.reference}-${store.sets[0]?.key.slot}`;
expect(result).toEqual({
instanceId: 'alpha',
referenceId: 'legacy-secret:alpha:instance-password',
externalReference: storedReference,
configRevision: 4,
});
expect(
db.prepare('SELECT auth_mode,config_revision FROM instances WHERE id=?').get('alpha'),
).toEqual({
auth_mode: 'password',
config_revision: 4,
});
expect(
db
.prepare('SELECT id,instance_id,purpose,provider,external_reference FROM secret_references')
.get(),
).toEqual({
id: 'legacy-secret:alpha:instance-password',
instance_id: 'alpha',
purpose: 'instance-password',
provider: 'macos-keychain',
external_reference: storedReference,
});
const metadata = JSON.parse(
(
db
.prepare('SELECT value_json FROM app_settings WHERE key=?')
.get('legacy-import.instance.alpha') as { value_json: string }
).value_json,
);
expect(metadata).toMatchObject({ secretPending: false, requestedAuthMode: 'password' });
expect(JSON.stringify(metadata)).not.toContain(secret);
expect(allText(db)).not.toContain(secret);
});
it('rolls back every DB write and compensates keychain when the transaction fails', async () => {
const db = pendingDatabase();
const store = new FakeStore();
const before = state(db);
db.exec(
"CREATE TRIGGER fail_activation BEFORE INSERT ON secret_references BEGIN SELECT RAISE(ABORT, 'forced'); END",
);
const withTrigger = state(db);
await expect(
activatePendingSecret({ db, store, instanceId: 'alpha', secret }),
).rejects.toMatchObject({
code: 'DATABASE_FAILED',
});
expect(store.deletes).toHaveLength(1);
expect(store.deletes[0]).toBe(`${store.reference}-${store.sets[0]?.key.slot}`);
expect(state(db)).toEqual(withTrigger);
expect(withTrigger).not.toEqual(before);
});
it('returns a stable compensation error without leaking secret when cleanup fails', async () => {
const db = pendingDatabase();
db.exec(
"CREATE TRIGGER fail_activation BEFORE INSERT ON secret_references BEGIN SELECT RAISE(ABORT, 'forced'); END",
);
const store = new FakeStore();
store.deleteError = new Error(`malicious ${secret}`);
const error = await activatePendingSecret({ db, store, instanceId: 'alpha', secret }).catch(
(reason: unknown) => reason,
);
expect(error).toBeInstanceOf(ActivationError);
expect(error).toMatchObject({ code: 'COMPENSATION_FAILED' });
expect(
JSON.stringify({ name: (error as Error).name, message: (error as Error).message }),
).not.toContain(secret);
});
it('does not touch DB when store set fails', async () => {
const db = pendingDatabase();
const before = state(db);
const store = new FakeStore();
store.setError = new Error(`malicious ${secret}`);
const error = await activatePendingSecret({ db, store, instanceId: 'alpha', secret }).catch(
(reason: unknown) => reason,
);
expect(error).toMatchObject({ code: 'SECRET_STORE_FAILED' });
expect(state(db)).toEqual(before);
expect(
JSON.stringify({ name: (error as Error).name, message: (error as Error).message }),
).not.toContain(secret);
});
it('rejects duplicate activation before store access and leaves DB unchanged', async () => {
const db = pendingDatabase();
const firstStore = new FakeStore();
await activatePendingSecret({ db, store: firstStore, instanceId: 'alpha', secret });
const before = state(db);
const secondStore = new FakeStore();
await expect(
activatePendingSecret({ db, store: secondStore, instanceId: 'alpha', secret }),
).rejects.toMatchObject({ code: 'ALREADY_ACTIVE' });
expect(secondStore.sets).toHaveLength(0);
expect(state(db)).toEqual(before);
});
it('serializes concurrent activation so only one unique keychain item is created', async () => {
const db = pendingDatabase();
const store = new FakeStore();
const results = await Promise.allSettled([
activatePendingSecret({ db, store, instanceId: 'alpha', secret: `${secret}-one` }),
activatePendingSecret({ db, store, instanceId: 'alpha', secret: `${secret}-two` }),
]);
expect(results.filter(({ status }) => status === 'fulfilled')).toHaveLength(1);
expect(results.filter(({ status }) => status === 'rejected')).toHaveLength(1);
expect(store.sets).toHaveLength(1);
expect(store.sets[0]?.key.slot).toMatch(/^[A-Za-z0-9_-]+$/);
expect(store.deletes).toHaveLength(0);
const saved = db.prepare('SELECT external_reference FROM secret_references').get() as {
external_reference: string;
};
expect(saved.external_reference).toContain(store.sets[0]?.key.slot);
});
it('serializes activation across two managed connections to the same physical database', async () => {
const directory = await mkdtemp(join(tmpdir(), 'secret-activation-'));
const path = join(directory, 'state.sqlite');
const first = openDatabase(path);
const second = openDatabase(path);
migrateDatabase(first);
const now = '2026-07-16T00:00:00.000Z';
first
.prepare(
'INSERT INTO instances (id,name,base_url,auth_mode,enabled,config_revision,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)',
)
.run('alpha', 'Alpha', 'https://203.0.113.8', 'none', 1, 3, now, now);
first
.prepare('INSERT INTO app_settings VALUES (?,?,?,?)')
.run(
'legacy-import.instance.alpha',
JSON.stringify({ secretPending: true, requestedAuthMode: 'password' }),
now,
now,
);
const store = new FakeStore();
try {
const results = await Promise.allSettled([
activatePendingSecret({ db: first, store, instanceId: 'alpha', secret: `${secret}-one` }),
activatePendingSecret({ db: second, store, instanceId: 'alpha', secret: `${secret}-two` }),
]);
expect(results.filter(({ status }) => status === 'fulfilled')).toHaveLength(1);
expect(results.filter(({ status }) => status === 'rejected')).toHaveLength(1);
expect(store.sets).toHaveLength(1);
expect(store.deletes).toHaveLength(0);
} finally {
second.close();
first.close();
await rm(directory, { recursive: true, force: true });
}
});
it('rejects empty secrets and invalid pending metadata before store access', async () => {
const db = pendingDatabase();
const store = new FakeStore();
await expect(
activatePendingSecret({ db, store, instanceId: 'alpha', secret: '' }),
).rejects.toMatchObject({ code: 'EMPTY_SECRET' });
db.prepare('UPDATE app_settings SET value_json=?').run(
JSON.stringify({ secretPending: false, requestedAuthMode: 'password' }),
);
await expect(
activatePendingSecret({ db, store, instanceId: 'alpha', secret }),
).rejects.toMatchObject({ code: 'NOT_PENDING' });
expect(store.sets).toHaveLength(0);
});
});