diff --git a/src/plugin-host/settings.test.ts b/src/plugin-host/settings.test.ts index d87914b..3e7cb11 100644 --- a/src/plugin-host/settings.test.ts +++ b/src/plugin-host/settings.test.ts @@ -109,31 +109,26 @@ test("each type coerces from the environment, and a bad value fails loud", () => PLUGIN_SETTING_P_MODE: "loose", }); assert.equal(bad.errors.length, 4); - assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_COUNT.*number/); - assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_FLAG.*"true".*"false"/); - assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_MODE.*strict, lenient/); - assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_BASE.*URL/); + assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_COUNT.*number, got "twelve"/); + assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_FLAG.*"true".*"false", got "yes"/); + assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_MODE.*strict, lenient, got "loose"/); + assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_BASE.*URL, got "not a url"/); }); -test("a secret's bad value never reaches the boot error, whatever its type", () => { - const cases: [SettingDecl, string, RegExp][] = [ - [{ key: "s", secret: true, type: "boolean" }, "hunter2-bool", /PLUGIN_SETTING_P_S.*"true".*"false"/], - [{ key: "s", secret: true, type: "enum", values: ["a", "b"] }, "hunter2-enum", /PLUGIN_SETTING_P_S.*a, b/], - [{ key: "s", secret: true, type: "number" }, "hunter2-number", /PLUGIN_SETTING_P_S.*number/], - [{ key: "s", secret: true, type: "url" }, "postgres user:hunter2-url@db", /PLUGIN_SETTING_P_S.*URL/], - ]; - for (const [decl, raw, names] of cases) { +const SECRET_CASES: [SettingDecl, string, RegExp][] = [ + [{ key: "s", secret: true, type: "boolean" }, "hunter2-bool", /PLUGIN_SETTING_P_S.*"true".*"false"/], + [{ key: "s", secret: true, type: "enum", values: ["a", "b"] }, "hunter2-enum", /PLUGIN_SETTING_P_S.*a, b/], + [{ key: "s", secret: true, type: "number" }, "hunter2-number", /PLUGIN_SETTING_P_S.*number/], + [{ key: "s", secret: true, type: "url" }, "postgres user:hunter2-url@db", /PLUGIN_SETTING_P_S.*URL/], +]; +for (const [decl, raw, names] of SECRET_CASES) { + test(`a secret ${decl.type}'s bad value never reaches the boot error`, () => { const errors = resolveSettings([pluginWith("p", [decl])], { PLUGIN_SETTING_P_S: raw }).errors; - assert.equal(errors.length, 1, decl.type); - assert.match(errors[0] ?? "", names, decl.type); - assert.ok(!(errors[0] ?? "").includes("hunter2"), decl.type); - } -}); - -test("a non-secret's bad value is quoted in the boot error, so the operator sees the typo", () => { - const errors = resolveSettings([pluginWith("p", [{ key: "s", type: "url" }])], { PLUGIN_SETTING_P_S: "not a url" }).errors; - assert.match(errors[0] ?? "", /got "not a url"/); -}); + assert.equal(errors.length, 1); + assert.match(errors[0] ?? "", names); + assert.ok(!(errors[0] ?? "").includes("hunter2")); + }); +} test("a boolean is only \"true\"/\"false\" — a typo never degrades to false", () => { const plugins = [pluginWith("p", [{ default: true, key: "flag", type: "boolean" }])]; diff --git a/todo.md b/todo.md index 2867f6f..c52b36d 100644 --- a/todo.md +++ b/todo.md @@ -6,12 +6,13 @@ `Bar = 1` -`Next ID = 55` +`Next ID = 56` ## Items | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| +| 55 | 0.4.21 | | security | **Keep a URL config value out of the boot error that rejects it.** | 1 | 1 | 4 | 5 | 2 | | 0.0 | | 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 | | 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 | | 48 | 0.5.0 | | | **Correct the Keto tuple in the `login.ts` and `bootstrap.ts` comments to the `granted` relation the code writes.** | 1 | 1 | 1 | 1 | 1 | code health by 4 | 28.7 | @@ -33,6 +34,20 @@ ## Details +### 55. Keep a URL config value out of the boot error that rejects it. + +`readOptionalUrl` and `readUrl` in `src/config.ts` throw `config: ${key} is not a valid URL: ${value}`, +so a malformed value reaches the boot error and the log. A DSN whose password breaks parsing, such as +`postgres://admin:pa/ss@db:5432/plugins`, leaks that password. The keys that can carry credentials: +`PLUGIN_DB_URL` (`readCredentiallessUrl` refuses userinfo only after `readOptionalUrl` has parsed it, +so a pasted admin DSN that fails to parse is echoed), and `OTLP_ENDPOINT`, whose collector URL may +embed basic-auth userinfo. The other URL keys, `APP_URL`, `HYDRA_ADMIN_URL`, `JWKS_URL`, +`KETO_READ_URL`, `KETO_WRITE_URL`, `KRATOS_ADMIN_URL` and `KRATOS_PUBLIC_URL`, pass through the same +two readers, so the fix covers them too. Name the variable and what it accepts, never the value, and +add a test per credential-bearing key. The other readers in `config.ts` echo only non-credential +values. `PLUGIN_DB_ADMIN_URL` isn't read in `config.ts`: `bootstrap.ts` hands it straight to +`postgres()`. Check whether the driver echoes a malformed one. + ### 19. Run the documented Docker commands on macOS and fix whatever misbehaves. Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is