79 lines
5.3 KiB
JavaScript
79 lines
5.3 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawnSync } from 'node:child_process';
|
|
import test from 'node:test';
|
|
import { prepareResourcePlan, serializeResourcePlan } from './deploy-portal-resources.mjs';
|
|
|
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
const sha = 'a'.repeat(40);
|
|
const scriptSource = path.join(path.dirname(fileURLToPath(import.meta.url)), 'portal-resource-install.sh');
|
|
|
|
async function fixture(prefix = 'aia-resource-plan-') {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
const privateRoot = path.join(root, 'private');
|
|
const hubfs = path.join(privateRoot, 'hubfs');
|
|
const stage = path.join(root, 'staging', `portal-resources-${sha}`);
|
|
await fs.mkdir(hubfs, { recursive: true });
|
|
await fs.mkdir(path.join(root, 'secure-resources', 'staff'), { recursive: true });
|
|
await fs.mkdir(path.join(root, 'secure-resources', 'management'), { recursive: true });
|
|
return { root, privateRoot, hubfs, stage };
|
|
}
|
|
|
|
async function writeApprovedResource(privateRoot, hubfs, bytes = Buffer.from('synthetic non-phi document')) {
|
|
const localFile = 'a'.repeat(64);
|
|
await fs.writeFile(path.join(hubfs, localFile), bytes, { mode: 0o600 });
|
|
await fs.writeFile(path.join(privateRoot, 'manifest.json'), JSON.stringify({ resources: [{
|
|
classification: 'STAFF-INTERNAL', deliveryPath: '/staff-resources/guide.pdf', destinationImplemented: true, destinationVerified: true, destinationAccessControlled: true, rewriteApproved: true, localFile, sha256: digest(bytes), size: bytes.length,
|
|
}] }));
|
|
return { localFile, bytes };
|
|
}
|
|
|
|
test('resource plan copies only hash-verified approved protected resources', async () => {
|
|
const { root, privateRoot, hubfs, stage } = await fixture();
|
|
try {
|
|
const { localFile, bytes } = await writeApprovedResource(privateRoot, hubfs);
|
|
const plan = await prepareResourcePlan(privateRoot, stage, sha);
|
|
assert.equal(plan.files.length, 1);
|
|
assert.equal((await fs.readFile(path.join(stage, 'staff', localFile))).toString(), bytes.toString());
|
|
assert.match(serializeResourcePlan(plan), /^AIA_PORTAL_RESOURCE_PLAN_V1\nsourceCommit=a{40}\nfile\tstaff\tguide\.pdf\t/);
|
|
await fs.writeFile(path.join(privateRoot, 'manifest.json'), JSON.stringify({ resources: [{ classification: 'STAFF-INTERNAL', deliveryPath: '/staff-resources/../bad', localFile, sha256: digest(bytes), size: bytes.length }] }));
|
|
await assert.rejects(prepareResourcePlan(privateRoot, path.join(root, 'second'), sha), /destination/);
|
|
} finally { await fs.rm(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('portable shell installer executes the reviewed staged-plan interface with synthetic files', async () => {
|
|
const { root, privateRoot, hubfs, stage } = await fixture('aia-portal-install-');
|
|
try {
|
|
const { bytes } = await writeApprovedResource(privateRoot, hubfs);
|
|
const plan = await prepareResourcePlan(privateRoot, stage, sha);
|
|
await fs.mkdir(path.join(stage, 'management'), { recursive: true });
|
|
const planFile = path.join(root, 'approved-plan.tsv');
|
|
await fs.writeFile(planFile, serializeResourcePlan(plan), { mode: 0o600 });
|
|
const fakeBin = path.join(root, 'fake-bin');
|
|
const log = path.join(root, 'ownership.log');
|
|
await fs.mkdir(fakeBin);
|
|
await fs.writeFile(path.join(fakeBin, 'id'), '#!/bin/sh\n[ "$1" = -u ] && { echo 0; exit 0; }\nexec /usr/bin/id "$@"\n', { mode: 0o755 });
|
|
await fs.writeFile(path.join(fakeBin, 'getent'), '#!/bin/sh\n[ "$1" = group ] && [ "$2" = nginx ] && { echo "nginx:x:999:"; exit 0; }\nexec /run/current-system/sw/bin/getent "$@"\n', { mode: 0o755 });
|
|
await fs.writeFile(path.join(fakeBin, 'install'), `#!/bin/sh\nprintf '%s\\n' "$*" >> ${JSON.stringify(log)}\nwhile [ "$#" -gt 0 ]; do\n case "$1" in -o|-g|-m) shift 2 ;; *) break ;; esac\ndone\nexec /usr/bin/install -m 0640 "$@"\n`, { mode: 0o755 });
|
|
const script = path.join(root, 'portal-resource-install.sh');
|
|
const source = await fs.readFile(scriptSource, 'utf8');
|
|
assert.ok(!source.includes('node '));
|
|
await fs.writeFile(script, source.replace('ROOT=/srv/www/azinstitute4autism.com', `ROOT=${root}`), { mode: 0o755 });
|
|
const result = spawnSync('/bin/sh', [script, stage, planFile], { encoding: 'utf8', env: { ...process.env, PATH: process.env.PATH, AIA_PORTAL_INSTALL_TEST_PATH: fakeBin } });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal((await fs.readFile(path.join(root, 'secure-resources/staff/guide.pdf'))).toString(), bytes.toString());
|
|
assert.match(await fs.readFile(log, 'utf8'), /-o root -g nginx -m 0640/);
|
|
const nonRoot = spawnSync('/bin/sh', [script, stage, planFile], { encoding: 'utf8', env: { ...process.env, PATH: process.env.PATH } });
|
|
assert.notEqual(nonRoot.status, 0);
|
|
assert.match(nonRoot.stderr, /requires root/);
|
|
await fs.writeFile(path.join(stage, 'staff', 'unexpected.pdf'), 'unexpected');
|
|
const rejected = spawnSync('/bin/sh', [script, stage, planFile], { encoding: 'utf8', env: { ...process.env, PATH: process.env.PATH, AIA_PORTAL_INSTALL_TEST_PATH: fakeBin } });
|
|
assert.notEqual(rejected.status, 0);
|
|
assert.match(rejected.stderr, /Unexpected staged resource/);
|
|
} finally { await fs.rm(root, { recursive: true, force: true }); }
|
|
});
|