Keep a secret setting's raw value out of the boot error that rejects it #201
@@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### A plugin setting marked `secret` keeps its value out of the boot error that rejects it
|
||||
|
||||
A `secret` setting of type `boolean`, `enum`, `number` or `url` that fails validation names its
|
||||
variable and what it accepts, and no longer prints the value, so a DSN with a password doesn't reach
|
||||
the log. A setting that isn't secret still prints the value it got.
|
||||
|
||||
### The admin's recovery-code notice links to the recovery flow the code belongs to
|
||||
|
||||
After an admin generates a recovery code for a user, the notice asks them to send the user both the
|
||||
|
||||
@@ -274,3 +274,20 @@ Read most (opens and searches):
|
||||
- `src/i18n/locales/en-US.ts` 3
|
||||
- `e2e-tests/compose.full.yml` 2
|
||||
- `src/auth/kratos-admin.ts` 2
|
||||
|
||||
## Reading cost, chunk d8b41620d631, finished 2026-10-31T12:37:38Z
|
||||
|
||||
70583 new input tokens before the first commit.
|
||||
|
||||
Read most (opens and searches):
|
||||
|
||||
- `todo.md` 5
|
||||
- `AGENTS.md` 2
|
||||
- `README.md` 2
|
||||
- `examples/plugins/admin/README.md` 2
|
||||
- `src/auth/kratos-admin.ts` 2
|
||||
- `src/http/app-admin.test.ts` 2
|
||||
- `CHANGELOG.md` 1
|
||||
- `e2e-tests/full-flow.spec.ts` 1
|
||||
- `examples/plugins/admin/admin-users.test.ts` 1
|
||||
- `examples/plugins/admin/admin-users.ts` 1
|
||||
|
||||
@@ -115,6 +115,26 @@ test("each type coerces from the environment, and a bad value fails loud", () =>
|
||||
assert.match(bad.errors.join("\n"), /PLUGIN_SETTING_P_BASE.*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 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"/);
|
||||
});
|
||||
|
||||
test("a boolean is only \"true\"/\"false\" — a typo never degrades to false", () => {
|
||||
const plugins = [pluginWith("p", [{ default: true, key: "flag", type: "boolean" }])];
|
||||
assert.equal(resolveSettings(plugins, { PLUGIN_SETTING_P_FLAG: "false" }).values.get("p")?.["flag"], false);
|
||||
|
||||
@@ -231,26 +231,28 @@ function summarize(decl: SettingDecl, name: string, source: SettingSummary["sour
|
||||
};
|
||||
}
|
||||
|
||||
// A coerced value, or the boot error naming the variable and what it accepts.
|
||||
// A coerced value, or the boot error naming the variable and what it accepts. A secret's value
|
||||
// stays out of it: the error reaches the log.
|
||||
function coerce(decl: SettingDecl, raw: string, name: string): { value: SettingValue } | string {
|
||||
const got = decl.secret === true ? "" : `, got "${raw}"`;
|
||||
switch (decl.type) {
|
||||
case "boolean":
|
||||
if (raw === "true") return { value: true };
|
||||
if (raw === "false") return { value: false };
|
||||
return `${name} must be "true" or "false", got "${raw}"`;
|
||||
return `${name} must be "true" or "false"${got}`;
|
||||
case "enum":
|
||||
return (decl.values ?? []).includes(raw)
|
||||
? { value: raw }
|
||||
: `${name} must be one of ${(decl.values ?? []).join(", ")}, got "${raw}"`;
|
||||
: `${name} must be one of ${(decl.values ?? []).join(", ")}${got}`;
|
||||
case "number": {
|
||||
const value = Number(raw);
|
||||
return Number.isFinite(value) ? { value } : `${name} must be a number, got "${raw}"`;
|
||||
return Number.isFinite(value) ? { value } : `${name} must be a number${got}`;
|
||||
}
|
||||
case "url":
|
||||
try {
|
||||
new URL(raw);
|
||||
} catch {
|
||||
return `${name} is not a valid URL: ${raw}`;
|
||||
return `${name} must be a valid URL${got}`;
|
||||
}
|
||||
return { value: raw };
|
||||
case "string":
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 46 | 0.4.21 | | security | **Keep a secret setting's raw 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 |
|
||||
@@ -34,13 +33,6 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 46. Keep a secret setting's raw value out of the boot error that rejects it.
|
||||
|
||||
`coerce` in `src/plugin-host/settings.ts` puts `got "${raw}"` (boolean, enum, number) and `: ${raw}`
|
||||
(url) into the error it returns, whatever `secret` says. A secret of type `url`, such as a DSN with
|
||||
a password, that fails `new URL` reaches the boot error and so the log. Name the variable and what it
|
||||
accepts, never the value; test one secret of each type.
|
||||
|
||||
### 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
|
||||
|
||||
Reference in New Issue
Block a user