#!/usr/bin/env node // Тестовый стенд PostgreSQL, повторяющий роли pilot, но без суперпользователя-мигратора: // * bootstrap-админ (E2E_PG_ADMIN_URL) используется ТОЛЬКО здесь, как DBA/entrypoint образа; // * krona_migrator — NOSUPERUSER NOCREATEDB NOCREATEROLE, владелец БД и схемы public; // * krona_staff_app — NOSUPERUSER NOBYPASSRLS NOINHERIT, права выдают миграции 0003+. // Пароли генерируются crypto.randomBytes и пишутся только в env-файл с правами 0600. // Печатаются имена ролей/БД, но не пароли. import { randomBytes } from 'node:crypto'; import { writeFileSync } from 'node:fs'; import pg from 'pg'; const args = new Map(); for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], process.argv[i + 1]); const adminUrl = process.env.E2E_PG_ADMIN_URL; const envFile = args.get('--out'); const database = args.get('--db') ?? 'krona_staff_e2e'; if (!adminUrl || !envFile) { console.error( 'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out [--db name]', ); process.exit(2); } if (!/^[a-z_][a-z0-9_]{0,62}$/.test(database)) throw new Error('Invalid database name'); export async function provision() { const migratorPassword = randomBytes(24).toString('hex'); const appPassword = randomBytes(24).toString('hex'); const admin = new pg.Client({ connectionString: adminUrl, application_name: 'krona-e2e-provision', }); await admin.connect(); try { await admin.query(` DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN CREATE ROLE krona_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS; END IF; IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_staff_app') THEN CREATE ROLE krona_staff_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS; END IF; END $$`); const alterMigrator = await admin.query( "SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql", [migratorPassword], ); await admin.query(alterMigrator.rows[0].sql); const alterApp = await admin.query( "SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql", [appPassword], ); await admin.query(alterApp.rows[0].sql); await admin.query( `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [database], ); await admin.query(`DROP DATABASE IF EXISTS ${database}`); await admin.query( `CREATE DATABASE ${database} OWNER krona_migrator ENCODING 'UTF8' TEMPLATE template0`, ); } finally { await admin.end(); } const dbAdminUrl = new URL(adminUrl); dbAdminUrl.pathname = `/${database}`; const dbAdmin = new pg.Client({ connectionString: dbAdminUrl.toString() }); await dbAdmin.connect(); try { await dbAdmin.query('ALTER SCHEMA public OWNER TO krona_migrator'); await dbAdmin.query('REVOKE CREATE ON SCHEMA public FROM PUBLIC'); await dbAdmin.query(`REVOKE ALL ON DATABASE ${database} FROM PUBLIC`); await dbAdmin.query(`GRANT CONNECT ON DATABASE ${database} TO krona_staff_app`); } finally { await dbAdmin.end(); } const base = new URL(adminUrl); const url = (user, password) => { const u = new URL(base.toString()); u.username = user; u.password = password; u.pathname = `/${database}`; return u.toString(); }; const migratorUrl = url('krona_migrator', migratorPassword); const appUrl = url('krona_staff_app', appPassword); const lines = [ `DATABASE_MIGRATION_URL=${migratorUrl}`, `DATABASE_TEST_MIGRATION_URL=${migratorUrl}`, `DATABASE_TEST_URL=${appUrl}`, `DATABASE_URL=${appUrl}`, `POSTGRES_APP_PASSWORD=${appPassword}`, '', ]; writeFileSync(envFile, lines.join('\n'), { mode: 0o600 }); // Проверяем атрибуты ролей и печатаем их (без паролей) как доказательство. const check = new pg.Client({ connectionString: migratorUrl }); await check.connect(); try { const roles = await check.query( `SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolbypassrls FROM pg_roles WHERE rolname IN ('krona_migrator','krona_staff_app') ORDER BY rolname`, ); const version = await check.query('SHOW server_version'); console.log( `provisioned database=${database} server_version=${version.rows[0].server_version}`, ); for (const r of roles.rows) { console.log( `role=${r.rolname} super=${r.rolsuper} createrole=${r.rolcreaterole} createdb=${r.rolcreatedb} bypassrls=${r.rolbypassrls}`, ); } } finally { await check.end(); } } await provision();