fix: harden staging database env validation
This commit is contained in:
@@ -17,6 +17,7 @@ brain/
|
||||
COVERAGE_AUDIT_REPORT.md
|
||||
*.tmp
|
||||
.env.local
|
||||
.env.staging.database
|
||||
.jyotish.local.env
|
||||
.coverage
|
||||
coverage.xml
|
||||
|
||||
@@ -43,12 +43,12 @@ fi
|
||||
|
||||
definition_count() {
|
||||
local key="$1"
|
||||
grep -Ec "^(export[[:space:]]+)?${key}=" "$ENV_FILE" || true
|
||||
grep -Ec "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" "$ENV_FILE" || true
|
||||
}
|
||||
|
||||
environment_value() {
|
||||
local key="$1"
|
||||
sed -n -E "s/^(export[[:space:]]+)?${key}=(.*)$/\\2/p" "$ENV_FILE"
|
||||
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=[[:space:]]*(.*)$/\\2/p" "$ENV_FILE"
|
||||
}
|
||||
|
||||
require_once_non_empty() {
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
chmodSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL("../..", import.meta.url));
|
||||
const validator = join(repositoryRoot, "deploy/validate-staging-database-env.sh");
|
||||
const validEnvironment = [
|
||||
"POSTGRES_DB=jyotisha",
|
||||
"POSTGRES_USER=postgres",
|
||||
"POSTGRES_PASSWORD=postgres-test-password",
|
||||
"SCHEMA_OWNER_PASSWORD=schema-owner-test-password",
|
||||
"IDENTITY_RUNTIME_PASSWORD=identity-runtime-test-password",
|
||||
"APP_RUNTIME_PASSWORD=app-runtime-test-password",
|
||||
"ADMIN_RUNTIME_PASSWORD=admin-runtime-test-password",
|
||||
"MIGRATION_RUNNER_PASSWORD=migration-runner-test-password",
|
||||
"BACKUP_READER_PASSWORD=backup-reader-test-password",
|
||||
"STAGING_BACKUP_ENCRYPTION_KEY=staging-backup-test-password",
|
||||
"SCHEMA_DATABASE_URL=postgresql://schema_owner:schema-owner-test-password@postgres:5432/jyotisha",
|
||||
];
|
||||
|
||||
test("database env validator rejects Compose-compatible duplicate selectors without printing values", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-"));
|
||||
const envFile = join(root, ".env.staging.database");
|
||||
const composeFile = join(root, "compose.yml");
|
||||
|
||||
try {
|
||||
writeFileSync(
|
||||
composeFile,
|
||||
[
|
||||
"services:",
|
||||
" probe:",
|
||||
" image: alpine",
|
||||
" environment:",
|
||||
" SELECTED: ${POSTGRES_DB}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
envFile,
|
||||
`${[...validEnvironment, " POSTGRES_DB = evil"].join("\n")}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
chmodSync(envFile, 0o600);
|
||||
|
||||
const rendered = spawnSync(
|
||||
"docker",
|
||||
[
|
||||
"compose",
|
||||
"--env-file",
|
||||
envFile,
|
||||
"-f",
|
||||
composeFile,
|
||||
"config",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(rendered.status, 0, rendered.stderr);
|
||||
assert.equal(
|
||||
JSON.parse(rendered.stdout).services.probe.environment.SELECTED,
|
||||
"evil",
|
||||
);
|
||||
|
||||
const result = spawnSync("bash", [validator, envFile], { encoding: "utf8" });
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.doesNotMatch(
|
||||
`${result.stdout}${result.stderr}`,
|
||||
/postgres-test-password|schema-owner-test-password|staging-backup-test-password/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("database env validator rejects symlinks and unsafe modes", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-"));
|
||||
const envFile = join(root, ".env.staging.database");
|
||||
const target = join(root, "database-target.env");
|
||||
|
||||
try {
|
||||
writeFileSync(target, `${validEnvironment.join("\n")}\n`, { mode: 0o600 });
|
||||
chmodSync(target, 0o600);
|
||||
symlinkSync(target, envFile);
|
||||
assert.notEqual(
|
||||
spawnSync("bash", [validator, envFile], { encoding: "utf8" }).status,
|
||||
0,
|
||||
);
|
||||
|
||||
rmSync(envFile);
|
||||
writeFileSync(envFile, `${validEnvironment.join("\n")}\n`, { mode: 0o644 });
|
||||
chmodSync(envFile, 0o644);
|
||||
assert.notEqual(
|
||||
spawnSync("bash", [validator, envFile], { encoding: "utf8" }).status,
|
||||
0,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("database env validator accepts a private valid file without printing values", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-"));
|
||||
const envFile = join(root, ".env.staging.database");
|
||||
|
||||
try {
|
||||
writeFileSync(envFile, `${validEnvironment.join("\n")}\n`, { mode: 0o600 });
|
||||
chmodSync(envFile, 0o600);
|
||||
|
||||
const result = spawnSync("bash", [validator, envFile], { encoding: "utf8" });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, "staging database environment validated\n");
|
||||
assert.doesNotMatch(
|
||||
`${result.stdout}${result.stderr}`,
|
||||
/postgres-test-password|schema-owner-test-password|staging-backup-test-password/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("staging database environment file is ignored", () => {
|
||||
const result = spawnSync("git", ["check-ignore", ".env.staging.database"], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, ".env.staging.database\n");
|
||||
});
|
||||
@@ -17,19 +17,20 @@ test("database roles have no cluster privileges", () => {
|
||||
assert.equal(
|
||||
fixture.psql(`
|
||||
select rolname || ':' || rolsuper || ':' || rolcreatedb || ':' ||
|
||||
rolcreaterole || ':' || rolbypassrls
|
||||
rolcreaterole || ':' || rolbypassrls || ':' ||
|
||||
rolreplication || ':' || rolinherit
|
||||
from pg_roles
|
||||
where rolname in ('schema_owner','identity_runtime','app_runtime',
|
||||
'admin_runtime','migration_runner','backup_reader')
|
||||
order by rolname
|
||||
`),
|
||||
[
|
||||
"admin_runtime:f:f:f:f",
|
||||
"app_runtime:f:f:f:f",
|
||||
"backup_reader:f:f:f:f",
|
||||
"identity_runtime:f:f:f:f",
|
||||
"migration_runner:f:f:f:f",
|
||||
"schema_owner:f:f:f:f",
|
||||
"admin_runtime:f:f:f:f:f:f",
|
||||
"app_runtime:f:f:f:f:f:f",
|
||||
"backup_reader:f:f:f:f:f:f",
|
||||
"identity_runtime:f:f:f:f:f:f",
|
||||
"migration_runner:f:f:f:f:f:f",
|
||||
"schema_owner:f:f:f:f:f:f",
|
||||
].join("\n"),
|
||||
);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user