chore: database cleanup & roles

This commit is contained in:
Charlie Lamb
2026-04-21 12:57:31 +01:00
parent 7d82333094
commit f0133fcd7f
4 changed files with 183 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
import { Pool } from "pg";
const DEFAULT_ENV = "dev";
const DEFAULT_CRITICAL_ROLE = "autumn_critical";
const ROLE_SETTINGS = {
statement_timeout: "2s",
lock_timeout: "1s",
idle_in_transaction_session_timeout: "10s",
};
const quoteIdent = (value: string) => `"${value.replaceAll('"', '""')}"`;
const quoteLiteral = (value: string) => `'${value.replaceAll("'", "''")}'`;
const getArg = (name: string) => {
const prefix = `${name}=`;
const inlineArg = process.argv.find((arg) => arg.startsWith(prefix));
if (inlineArg) return inlineArg.slice(prefix.length);
const index = process.argv.indexOf(name);
return index === -1 ? undefined : process.argv[index + 1];
};
type InfisicalSecret = {
key?: string;
value?: string;
secretKey?: string;
secretValue?: string;
};
const setEnvFromInfisicalExport = (value: unknown) => {
if (Array.isArray(value)) {
for (const secret of value as InfisicalSecret[]) {
const key = secret.key ?? secret.secretKey;
const secretValue = secret.value ?? secret.secretValue;
if (key && secretValue) {
process.env[key] = secretValue;
}
}
return;
}
for (const [key, secretValue] of Object.entries(value as Record<string, string>)) {
process.env[key] = secretValue;
}
};
const loadInfisicalEnv = (env: string) => {
const result = Bun.spawnSync([
"infisical",
"secrets",
"--env",
env,
"--output",
"json",
"--recursive",
"--silent",
]);
if (!result.success) {
throw new Error(
`Failed to load Infisical env "${env}": ${result.stderr.toString()}`,
);
}
setEnvFromInfisicalExport(JSON.parse(result.stdout.toString()));
};
const main = async () => {
const env = getArg("--env") ?? DEFAULT_ENV;
const criticalRole = getArg("--role") ?? DEFAULT_CRITICAL_ROLE;
loadInfisicalEnv(env);
const criticalDatabaseUrl = process.env.DATABASE_CRITICAL_URL;
if (!criticalDatabaseUrl) {
throw new Error(`DATABASE_CRITICAL_URL is not set in Infisical env "${env}"`);
}
const client = new Pool({
connectionString: criticalDatabaseUrl,
max: 1,
});
try {
for (const [key, value] of Object.entries(ROLE_SETTINGS)) {
await client.query(
`ALTER ROLE ${quoteIdent(criticalRole)} SET ${key} = ${quoteLiteral(
value,
)}`,
);
}
const result = await client.query<{ rolconfig: string[] | null }>(
"SELECT rolconfig FROM pg_roles WHERE rolname = $1",
[criticalRole],
);
console.log(`Updated ${criticalRole} in ${env}:`);
console.log(result.rows[0]?.rolconfig ?? []);
console.log("Verified via DATABASE_CRITICAL_URL:");
for (const key of Object.keys(ROLE_SETTINGS)) {
const verifyResult = await client.query<Record<string, string>>(`SHOW ${key}`);
console.log(`${key}=${verifyResult.rows[0]?.[key]}`);
}
console.log("Restart app connections for existing pools to pick this up.");
} finally {
await client.end();
}
};
await main();

View File

@@ -0,0 +1,43 @@
import { createHash } from "node:crypto";
const hash = (value: string) =>
createHash("sha256").update(value).digest("hex").slice(0, 12);
const mask = (value = "") =>
value.length > 6
? `${value.slice(0, 3)}***${value.slice(-3)}`
: `${value[0] ?? ""}***${value.slice(-1)}`;
const auth = (url: URL) => {
const username = decodeURIComponent(url.username);
const password = decodeURIComponent(url.password);
if (!username && !password) return "";
return `${mask(username)}${password ? `:${mask(password)}` : ""}@`;
};
const formatUrl = (value: string) => {
const url = new URL(value);
return `${url.protocol}//${auth(url)}${url.host}${url.pathname}${
url.search ? "?<redacted>" : ""
}`;
};
export const redactDatabaseUrl = (databaseUrl?: string) => {
const value = databaseUrl?.trim();
if (!value) return "unset";
try {
return `${formatUrl(value)} #${hash(value)}`;
} catch {
return `<invalid database url> #${hash(value)}`;
}
};
export const getRedactedDatabaseUrls = () => ({
primary: redactDatabaseUrl(process.env.DATABASE_URL),
replica: redactDatabaseUrl(process.env.DATABASE_REPLICA_URL),
critical: redactDatabaseUrl(
process.env.DATABASE_CRITICAL_URL || process.env.DATABASE_URL,
),
});

View File

@@ -10,6 +10,7 @@ import {
initPgHealthMonitor,
shutdownPgHealthMonitor,
} from "./db/pgHealthMonitor.js";
import { getRedactedDatabaseUrls } from "./db/redactDatabaseUrl.js";
import { logger } from "./external/logtail/logtailUtils.js";
import {
startAllEdgeConfigPolling,
@@ -38,6 +39,8 @@ import { startMemoryMonitor } from "./utils/memoryMonitor.js";
checkEnvVars();
const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
logger.info(getRedactedDatabaseUrls(), "DB URLs");
const app = createHonoApp();
initPgHealthMonitor({ client: clientCritical });

View File

@@ -0,0 +1,24 @@
import { describe, expect, test } from "bun:test";
import { redactDatabaseUrl } from "@/db/redactDatabaseUrl.js";
describe("redactDatabaseUrl", () => {
test("redacts credentials and query string", () => {
const redacted = redactDatabaseUrl(
"postgres://user:secret@db.example.com:5432/autumn?sslmode=require",
);
expect(redacted).toMatch(
/^postgres:\/\/u\*\*\*r:s\*\*\*t@db\.example\.com:5432\/autumn\?<redacted> #[a-f0-9]{12}$/,
);
expect(redacted).not.toContain("user");
expect(redacted).not.toContain("secret");
expect(redacted).not.toContain("sslmode=require");
});
test("does not echo invalid urls", () => {
const redacted = redactDatabaseUrl("not a url with secret");
expect(redacted).toMatch(/^<invalid database url> #[a-f0-9]{12}$/);
expect(redacted).not.toContain("not a url with secret");
});
});