chore: database cleanup & roles
This commit is contained in:
113
scripts/db/set-critical-role-timeouts.ts
Normal file
113
scripts/db/set-critical-role-timeouts.ts
Normal 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();
|
||||||
43
server/src/db/redactDatabaseUrl.ts
Normal file
43
server/src/db/redactDatabaseUrl.ts
Normal 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,
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
initPgHealthMonitor,
|
initPgHealthMonitor,
|
||||||
shutdownPgHealthMonitor,
|
shutdownPgHealthMonitor,
|
||||||
} from "./db/pgHealthMonitor.js";
|
} from "./db/pgHealthMonitor.js";
|
||||||
|
import { getRedactedDatabaseUrls } from "./db/redactDatabaseUrl.js";
|
||||||
import { logger } from "./external/logtail/logtailUtils.js";
|
import { logger } from "./external/logtail/logtailUtils.js";
|
||||||
import {
|
import {
|
||||||
startAllEdgeConfigPolling,
|
startAllEdgeConfigPolling,
|
||||||
@@ -38,6 +39,8 @@ import { startMemoryMonitor } from "./utils/memoryMonitor.js";
|
|||||||
checkEnvVars();
|
checkEnvVars();
|
||||||
|
|
||||||
const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
|
const init = async ({ startupStartedAt }: { startupStartedAt: number }) => {
|
||||||
|
logger.info(getRedactedDatabaseUrls(), "DB URLs");
|
||||||
|
|
||||||
const app = createHonoApp();
|
const app = createHonoApp();
|
||||||
|
|
||||||
initPgHealthMonitor({ client: clientCritical });
|
initPgHealthMonitor({ client: clientCritical });
|
||||||
|
|||||||
24
server/tests/unit/db/redactDatabaseUrl.test.ts
Normal file
24
server/tests/unit/db/redactDatabaseUrl.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user