Keep a URL config value out of the boot error that rejects it
CI / full-gate (push) Failing after 1m6s
CI / full-gate (push) Failing after 1m6s
This commit is contained in:
@@ -181,6 +181,16 @@ test("provisioning still runs with nothing to provision, so orphans are reported
|
||||
assert.deepEqual(calls[0]?.pluginIds, []);
|
||||
});
|
||||
|
||||
// The postgres driver rethrows a malformed DSN with the whole string on the error (`input`), so
|
||||
// the URL is checked here, before the driver sees it.
|
||||
test("a malformed PLUGIN_DB_ADMIN_URL is refused by name, never echoing the password", async () => {
|
||||
const env = { PLUGIN_DB_ADMIN_URL: "postgres://admin:pa/ss@db:5432/plugins" };
|
||||
await assert.rejects(
|
||||
provisionPluginStorage(env, [storagePlugin("things")], SILENT, async () => ({ orphans: [], provisioned: [] })),
|
||||
(err: Error) => err.message.includes("PLUGIN_DB_ADMIN_URL") && !err.message.includes("pa/ss"),
|
||||
);
|
||||
});
|
||||
|
||||
test("a plugin declaring storage without a provisioning DSN fails loud, naming the plugin", async () => {
|
||||
const { calls, provision } = recordingProvisioner();
|
||||
await assert.rejects(
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// Then prints a first-run banner; fails loud on any unexpected upstream error.
|
||||
import { existsSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePluginDbConnectionLimit, resolvePluginDbSecret } from "../config.ts";
|
||||
import { assertUrl, resolvePluginDbConnectionLimit, resolvePluginDbSecret } from "../config.ts";
|
||||
import { discoverPlugins } from "../plugin-host/discovery.ts";
|
||||
import { declaredPermissions, isValidPermissionName, type Plugin } from "../plugin-host/plugin.ts";
|
||||
import { provisionStorage } from "../plugin-host/storage-provisioning.ts";
|
||||
@@ -179,6 +179,7 @@ export async function provisionPluginStorage(env: Env, plugins: Plugin[], log: L
|
||||
// Provisioned here, connected to from web: a different server means the role is created in one
|
||||
// place and looked for in another, surfacing inside a plugin as "password authentication failed".
|
||||
// Warned, not refused — web reaching a pooler that cannot run CREATE DATABASE is a legitimate split.
|
||||
assertUrl("PLUGIN_DB_ADMIN_URL", adminUrl);
|
||||
const mismatch = serverMismatch(adminUrl, env["PLUGIN_DB_URL"]);
|
||||
if (mismatch) log.warn("PLUGIN_DB_ADMIN_URL and PLUGIN_DB_URL name different servers", { servers: mismatch });
|
||||
const result = await provision({
|
||||
@@ -199,7 +200,7 @@ export async function provisionPluginStorage(env: Env, plugins: Plugin[], log: L
|
||||
export function serverMismatch(adminUrl: string, webUrl: string | undefined): string | null {
|
||||
if (!webUrl) return null;
|
||||
const [admin, web] = [safeHostPort(adminUrl), safeHostPort(webUrl)];
|
||||
if (admin === null || web === null || admin === web) return null; // a malformed URL fails in config.ts
|
||||
if (admin === null || web === null || admin === web) return null; // a malformed URL fails in assertUrl
|
||||
return `${admin} vs ${web}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,29 @@ test("PLUGIN_DB_URL carrying credentials is refused, not silently overwritten",
|
||||
assert.equal(loadConfig({}).pluginDbUrl, undefined); // unset ⇒ storage off
|
||||
});
|
||||
|
||||
// A DSN whose password breaks URL parsing (a bare "/") must not reach the boot error or the log.
|
||||
// The credential-bearing keys get their own value; the rest share the two readers, so one loop covers them.
|
||||
test("a malformed URL value is refused by naming the variable, never echoing the value", () => {
|
||||
const bad: Record<string, string> = {
|
||||
APP_URL: "https://acme:pa/ss@[host",
|
||||
HYDRA_ADMIN_URL: "http://u:pa/ss@[hydra:4445",
|
||||
JWKS_URL: "http://u:pa/ss@[jwks",
|
||||
KETO_READ_URL: "http://u:pa/ss@[keto:4466",
|
||||
KETO_WRITE_URL: "http://u:pa/ss@[keto:4467",
|
||||
KRATOS_ADMIN_URL: "http://u:pa/ss@[kratos:4434",
|
||||
KRATOS_PUBLIC_URL: "http://u:pa/ss@[kratos:4433",
|
||||
OTLP_ENDPOINT: "http://otel:pa/ss@[collector:4318",
|
||||
PLUGIN_DB_URL: "postgres://admin:pa/ss@db:5432/plugins",
|
||||
};
|
||||
for (const [key, value] of Object.entries(bad)) {
|
||||
assert.throws(
|
||||
() => loadConfig({ [key]: value }),
|
||||
(err: Error) => err.message.includes(key) && !err.message.includes("pa/ss") && !err.message.includes(value),
|
||||
key,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the per-role connection ceiling defaults to 10 and rejects nonsense", () => {
|
||||
assert.equal(resolvePluginDbConnectionLimit({}), 10);
|
||||
assert.equal(resolvePluginDbConnectionLimit({ PLUGIN_DB_CONNECTION_LIMIT: "25" }), 25);
|
||||
|
||||
+12
-10
@@ -98,26 +98,28 @@ function readEnum<T extends string>(env: Env, key: string, allowed: readonly T[]
|
||||
throw new Error(`config: ${key} must be one of ${allowed.join(", ")}, got "${value}"`);
|
||||
}
|
||||
|
||||
// An optional absolute URL: unset/empty ⇒ undefined; a set-but-malformed value fails at boot.
|
||||
function readOptionalUrl(env: Env, key: string): string | undefined {
|
||||
const value = env[key] || undefined;
|
||||
if (value === undefined) return undefined;
|
||||
// Never echoes the value: a DSN whose password breaks URL parsing (a bare "/") would put the
|
||||
// password in the boot error and the log. bootstrap uses it too, before the postgres driver, which
|
||||
// rethrows a malformed DSN with the whole string attached.
|
||||
export function assertUrl(key: string, value: string): void {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
throw new Error(`config: ${key} is not a valid URL: ${value}`);
|
||||
throw new Error(`config: ${key} must be an absolute URL (its value is not shown; percent-encode special characters in a password)`);
|
||||
}
|
||||
}
|
||||
|
||||
// An optional absolute URL: unset/empty ⇒ undefined; a set-but-malformed value fails at boot.
|
||||
function readOptionalUrl(env: Env, key: string): string | undefined {
|
||||
const value = env[key] || undefined;
|
||||
if (value !== undefined) assertUrl(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// An absolute URL: defaults to the Ory service; validated so a typo fails at boot.
|
||||
function readUrl(env: Env, key: string, devDefault: string): string {
|
||||
const value = env[key] ?? devDefault;
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
throw new Error(`config: ${key} is not a valid URL: ${value}`);
|
||||
}
|
||||
assertUrl(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user