Warn rather than refuse on a storage URL mismatch, and scrub the provisioning DSN before discovery
CI / full-gate (push) Successful in 2m59s
CI / full-gate (push) Successful in 2m59s
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { differentServer, ensureJwks, firstRunBanner, identityPayload, permissionTuple, provisionPluginStorage, seedAdmin, seedPermissions } from "./bootstrap.ts";
|
||||
import { ensureJwks, firstRunBanner, identityPayload, permissionTuple, provisionPluginStorage, seedAdmin, seedPermissions, serverMismatch } from "./bootstrap.ts";
|
||||
import { createLogger } from "../logger.ts";
|
||||
import type { Plugin } from "../plugin-host/plugin.ts";
|
||||
import type { ProvisionOptions, ProvisionResult } from "../plugin-host/storage-provisioning.ts";
|
||||
@@ -200,16 +200,17 @@ test("the connection limit and derived secret reach the provisioner", async () =
|
||||
});
|
||||
|
||||
// bootstrap creates the role on one server; web tells the plugin to connect to another. Left
|
||||
// unchecked it surfaces inside a plugin as "password authentication failed", naming neither.
|
||||
test("provisioning refuses when the two storage URLs name different servers", async () => {
|
||||
// unsaid it surfaces inside a plugin as "password authentication failed", naming neither. Warned
|
||||
// rather than refused: web reaching a pooler bootstrap cannot provision through is legitimate.
|
||||
test("a storage URL mismatch is reported, and provisioning still runs", async () => {
|
||||
const { calls, provision } = recordingProvisioner();
|
||||
const env = { PLUGIN_DB_ADMIN_URL: "postgres://ory:ory@db-a:5432/ory", PLUGIN_DB_URL: "postgres://db-b:5432" };
|
||||
await assert.rejects(provisionPluginStorage(env, [storagePlugin("things")], SILENT, provision), /one server.*db-a:5432 vs db-b:5432/s);
|
||||
assert.deepEqual(calls, []);
|
||||
await provisionPluginStorage(env, [storagePlugin("things")], SILENT, provision);
|
||||
assert.equal(calls.length, 1);
|
||||
});
|
||||
|
||||
test("the same server spelled with an implicit port still agrees", () => {
|
||||
assert.equal(differentServer("postgres://ory:ory@db:5432/ory", "postgres://db"), null); // 5432 is the default
|
||||
assert.equal(differentServer("postgres://ory:ory@db:5432/ory", undefined), null); // web's own boot error to raise
|
||||
assert.equal(differentServer("postgres://ory:ory@db:5432/ory", "postgres://db:6543"), "db:5432 vs db:6543");
|
||||
assert.equal(serverMismatch("postgres://ory:ory@db:5432/ory", "postgres://db"), null); // 5432 is the default
|
||||
assert.equal(serverMismatch("postgres://ory:ory@db:5432/ory", undefined), null); // web's own boot error to raise
|
||||
assert.equal(serverMismatch("postgres://ory:ory@db:5432/ory", "postgres://db:6543"), "db:5432 vs db:6543");
|
||||
});
|
||||
|
||||
+11
-6
@@ -146,7 +146,7 @@ export function firstRunBanner(opts: { appUrl: string; email: string; password:
|
||||
// --- CLI (the bootstrap container entrypoint) ----------------------------------------
|
||||
|
||||
async function main() {
|
||||
const env = process.env;
|
||||
const env = { ...process.env }; // snapshot: the storage credentials leave process.env before discovery
|
||||
// Structured like the web app so prod logs stay uniform; honour LOG_FORMAT/SERVICE_NAME.
|
||||
const log = createLogger({
|
||||
format: env["LOG_FORMAT"] === "json" ? "json" : "text",
|
||||
@@ -155,6 +155,10 @@ async function main() {
|
||||
// runWithLog makes `log` ambient so seedAdmin's tracedFetch traces the Kratos/Keto seed calls.
|
||||
await runWithLog(log, async () => {
|
||||
if (ensureJwks(env["JWKS_FILE"] ?? "/etc/config/kratos/tokenizer/jwks.json")) log.info("generated a JWKS signing key");
|
||||
// Discovery imports every plugin module — and its dependencies — into *this* process, which holds
|
||||
// the credential that may CREATE DATABASE/ROLE. Same move as server.ts, on the stronger secret.
|
||||
delete process.env["PLUGIN_DB_ADMIN_URL"];
|
||||
delete process.env["PLUGIN_DB_SECRET"];
|
||||
const plugins = await discoverPlugins();
|
||||
await provisionPluginStorage(env, plugins, log);
|
||||
await seedAdminAndPermissions(env, plugins, log);
|
||||
@@ -172,10 +176,11 @@ export async function provisionPluginStorage(env: Env, plugins: Plugin[], log: L
|
||||
// last storage plugin is exactly when an orphaned database needs naming.
|
||||
if (ids.length === 0 && !adminUrl) return;
|
||||
if (!adminUrl) throw new Error(`bootstrap: PLUGIN_DB_ADMIN_URL must be set — these plugins declare storage: ${ids.join(", ")}`);
|
||||
// Provisioned here, connected to from web: a different server means the role exists in one place
|
||||
// and is looked for in another, surfacing as "password authentication failed" inside a plugin.
|
||||
const serverMismatch = differentServer(adminUrl, env["PLUGIN_DB_URL"]);
|
||||
if (serverMismatch) throw new Error(`bootstrap: PLUGIN_DB_ADMIN_URL and PLUGIN_DB_URL must name one server (${serverMismatch})`);
|
||||
// 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.
|
||||
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({
|
||||
adminUrl,
|
||||
connectionLimit: resolvePluginDbConnectionLimit(env),
|
||||
@@ -191,7 +196,7 @@ export async function provisionPluginStorage(env: Env, plugins: Plugin[], log: L
|
||||
|
||||
// Describes the disagreement, or null when they agree (or when web's URL is unset — that is web's
|
||||
// own boot error to raise, naming the plugin that wanted storage).
|
||||
export function differentServer(adminUrl: string, webUrl: string | undefined): string | null {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user