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', mappingApproved: true, accessPolicyApproved: true, rewriteApproved: true, destinationInstalled: false, destinationHashVerified: false, destinationPermissionsVerified: false, destinationAccessVerified: false, 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', mappingApproved: true, accessPolicyApproved: true, rewriteApproved: true, localFile, sha256: digest(bytes), size: bytes.length }] })); await assert.rejects(prepareResourcePlan(privateRoot, path.join(root, 'second'), sha), /approved for installation/); } finally { await fs.rm(root, { recursive: true, force: true }); } }); test('one verified source may be installed into both protected role namespaces', async () => { const { root, privateRoot, hubfs, stage } = await fixture(); try { const localFile = 'b'.repeat(64), bytes = Buffer.from('synthetic shared non-phi document'); await fs.writeFile(path.join(hubfs, localFile), bytes, { mode: 0o600 }); const mapping = (role) => ({ role, classification: role === 'staff' ? 'STAFF-INTERNAL' : 'MANAGEMENT-INTERNAL', deliveryPath: `/${role}-resources/shared.pdf`, mappingApproved: true, accessPolicyApproved: true, rewriteApproved: true, destinationInstalled: false, destinationHashVerified: false, destinationPermissionsVerified: false, destinationAccessVerified: false }); await fs.writeFile(path.join(privateRoot, 'manifest.json'), JSON.stringify({ resources: [{ classification: 'UNRESOLVED', installMappings: [mapping('staff'), mapping('management')], localFile, sha256: digest(bytes), size: bytes.length }] })); const plan = await prepareResourcePlan(privateRoot, stage, sha); assert.deepEqual(plan.files.map(({ role }) => role), ['management', 'staff']); assert.equal((await fs.readFile(path.join(stage, 'staff', localFile))).toString(), bytes.toString()); assert.equal((await fs.readFile(path.join(stage, 'management', localFile))).toString(), bytes.toString()); } 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 }); } });