Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798import { test } from 'node:test'import assert from 'node:assert/strict'import { randomUUID, generateKeyPairSync } from 'node:crypto'import { execFileSync, spawnSync } from 'node:child_process'import { accountLoginScope } from '../src/auth/integration-oauth-scopes.mjs'
test('Nix image runs nonroot/read-only, refuses a second writer and recovers the same broker after SIGKILL', { timeout: 120_000 }, async () => { const image = process.env.REAMS_CONTAINER_IMAGE assert.ok(image, 'Set REAMS_CONTAINER_IMAGE to the loaded Nix-built test image') const id = randomUUID(), name = `reams-test-${id}`, other = `${name}-other`, volume = `${name}-data` const secrets = `${name}-secrets` const privateKey = generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ type: 'pkcs8', format: 'pem' }) const config = JSON.parse(docker('image', 'inspect', image))[0].Config const base = JSON.parse(docker('run', '--rm', '--entrypoint', 'node', image, '-e', 'console.log(require("fs").readFileSync("/app/dist/atproto/deployment-config.json","utf8"))')) const origin = new URL(base.clientId).origin const env = { OAUTH_CLIENT_ID: base.clientId, OAUTH_REDIRECT_URI: base.redirectUri, OAUTH_SCOPE: base.scope, OAUTH_SIGNING_KEY: '/run/secrets/key.pem', OAUTH_KID: 'container-test-key', REAMS_UPDATE_DIAGNOSTICS: '1', } const args = ['--read-only', '--cap-drop=ALL', '--security-opt=no-new-privileges', '--stop-timeout', '35', '--tmpfs', '/tmp:rw,nosuid,nodev', '--mount', `type=volume,src=${volume},dst=/data`, '--mount', `type=volume,src=${secrets},dst=/run/secrets,readonly`, ...Object.entries(env).flatMap(([key, value]) => ['--env', `${key}=${value}`])] try { assert.equal(config.User, '1000:1000') assert.equal(config.StopSignal, 'SIGTERM') docker('volume', 'create', volume) docker('volume', 'create', secrets) // Stdin + a disposable mounted volume also works with a remote Docker // daemon; no host bind mount, image layer or CLI argument contains the key. execFileSync('docker', ['run', '-i', '--rm', '--user', '0:0', '--mount', `type=volume,src=${secrets},dst=/run/secrets`, '--entrypoint', 'node', image, '-e', 'const fs=require("fs");fs.writeFileSync("/run/secrets/key.pem",fs.readFileSync(0),{mode:0o400});fs.chownSync("/run/secrets/key.pem",1000,1000)'], { input: privateKey }) docker('run', '-d', '--name', name, ...args, image) await ready(name) assert.equal(docker('exec', name, 'node', '-e', 'console.log(require("fs").readFileSync("/proc/1/comm","utf8").trim())').trim(), 'node') const metadata = request(name, '/oauth-client-metadata.json', base) assert.equal(metadata.status, 200) assert.equal(JSON.parse(metadata.body).client_id, base.clientId) assert.equal(request(name, '/update-status.html', base).status, 200) assert.equal(request(name, '/oauth/broker/session', base, { host: 'wrong.test', 'x-forwarded-host': new URL(origin).host }).status, 404) const session = request(name, '/oauth/broker/session', base) assert.equal(session.status, 200) assert.match(session.cookie, /Secure; HttpOnly; SameSite=Strict/) const headers = { cookie: session.cookie.split(';')[0], 'x-oauth-csrf': JSON.parse(session.body).csrf_token } const before = docker('exec', name, 'node', '-e', 'console.log(require("fs").readFileSync(process.env.OAUTH_STORE_PATH,"utf8"))') const competing = spawnSync('docker', ['run', '--rm', '--name', other, ...args, image], { encoding: 'utf8', timeout: 10_000 }) assert.equal(competing.status, 1, competing.stderr) assert.equal(docker('exec', name, 'node', '-e', 'console.log(require("fs").readFileSync(process.env.OAUTH_STORE_PATH,"utf8"))'), before) docker('kill', '--signal', 'KILL', name) docker('rm', name) docker('run', '-d', '--name', name, ...args, image) await ready(name) assert.equal(JSON.parse(request(name, '/oauth/broker/session', base, headers).body).csrf_token, JSON.parse(session.body).csrf_token) docker('stop', '--time', '35', name) assert.equal(JSON.parse(docker('inspect', name))[0].State.ExitCode, 0) docker('rm', name) docker('run', '-d', '--name', name, ...args, image) await ready(name) assert.equal(JSON.parse(request(name, '/oauth/broker/session', base, headers).body).csrf_token, JSON.parse(session.body).csrf_token) // A production image must fail closed if runtime identity disagrees with its build. docker('stop', '--time', '35', name) docker('rm', name) const mismatch = spawnSync('docker', ['run', '--rm', ...args, '--env', `OAUTH_REDIRECT_URI=${origin}/wrong`, image], { encoding: 'utf8', timeout: 10_000 }) assert.notEqual(mismatch.status, 0) assert.match(mismatch.stderr, /disagrees with the compiled/) assert.equal(base.scope, accountLoginScope(base.scope)) } catch (error) { const logs = spawnSync('docker', ['logs', name], { encoding: 'utf8' }) console.error(logs.stdout, logs.stderr) throw error } finally { spawnSync('docker', ['rm', '-f', name, other], { stdio: 'ignore' }) spawnSync('docker', ['volume', 'rm', volume], { stdio: 'ignore' }) spawnSync('docker', ['volume', 'rm', secrets], { stdio: 'ignore' }) }})
function docker(...args) { return execFileSync('docker', args, { encoding: 'utf8', timeout: 40_000 }).trim() }function request(name, route, build, headers = {}) { return JSON.parse(docker('exec', name, 'node', '-e', ` const http = require('http'); http.get({host:'127.0.0.1',port:process.env.PORT,path:${JSON.stringify(route)},headers:${JSON.stringify({ host: new URL(build.clientId).host, ...headers })}}, async r => { let body = ''; for await (const c of r) body += c; console.log(JSON.stringify({status:r.statusCode,body,cookie:r.headers['set-cookie']?.[0]})); }).on('error', e => {console.error(e);process.exit(1)}); `))}async function ready(name) { for (let attempt = 0; attempt < 50; attempt++) { const result = spawnSync('docker', ['exec', name, 'node', '-e', "fetch('http://127.0.0.1:'+process.env.PORT+'/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"], { timeout: 3000 }) if (result.status === 0) return if (!JSON.parse(docker('inspect', name))[0].State.Running) throw new Error('Container exited before readiness') await new Promise(resolve => setTimeout(resolve, 100)) } throw new Error('Container did not become ready')}