fix(server): redact connection-test errors and restore duckdb/ducklake testability - #924
Conversation
|
Nice tight follow-up — the redaction, the A couple of things worth a look before merge (no verdict from me, just flags): 1. A third raw log boundary the redaction pass didn't cover. The description says the two log lines on these paths are redacted, but there's a third: const errorMessage = `Attached database '${attachedDb.name}' (${attachedDb.type}) test failed: ${(error as Error).message}`;
logger.error(errorMessage); // connection.ts:1775 — raw
failedAttachments.push(errorMessage);and logs it raw at Whether it fires for the connect-refused case the new specs exercise depends on eager-vs-lazy attach: the postgres extension normally connects at 2. The cwd fix makes a dormant file-write side effect live. Restoring the attach path means every Minor, non-blocking: |
de4ba3b to
8cf6f1a
Compare
|
Reviewed the diff against the tree, ran the new specs locally, and probed one behavior I wasn't sure about. The three claimed fixes are real and correctly implemented. I traced the pass-2 residual by hand ( One finding I'd want fixed before merge, because the new cleanup deletes files the connection test never created. 1. The
|
…e testability testConnectionConfig returned attach errors verbatim in the connection-test REST response body, and DuckDB attach failures echo the full connection string, so a failed test of a postgres/ducklake/duckdb-attached connection sent the cleartext password to the API caller. The returned errorMessage now goes through redactPgSecrets, as do the log lines on the attach path (testConnectionConfig's catch, attachDatabasesToDuckDB's catch, testDuckDBConnection's per-attachment catch, and isDatabaseAttached's catch). The specs for that fix surfaced a second bug: since #682 the throwaway config was built with an empty environment path, so DuckDB rejected the empty workingDirectory before any attach ran and duckdb/ducklake connection tests always failed with a validation error. The config is now rooted in a fresh temp directory (fs.mkdtemp), which is removed in the finally. Rooting it there rather than cwd means a connection test never reads, writes, or deletes an operator's own <name>.duckdb, and concurrent tests of one name can't clobber each other. Connection names for duckdb/ducklake become a <name>.duckdb filename, so an unsafe name is rejected: the controller returns 400 for one (consistent with its other request checks), and testConnectionConfig keeps a scoped assertSafePackageName as defense in depth. The check is scoped to the two filesystem-deriving types so a Postgres or BigQuery connection with any name stays testable. Also tightens the pg_helpers pass-2 comment per housejester's note on #915 (the raw-/ mop-up only recovers the password when no raw @ precedes the /) and pins that accepted residual with a spec. Signed-off-by: Monty Lennie <montylennie@gmail.com>
|
Thanks Kyle, this is a great review. Finding 1 is a real regression I introduced in the last round, and your temp-dir suggestion is clearly the right fix. All three are addressed (force-pushed, rebased onto current main).
On the smaller notes: the log now keeps the redacted stack ( Left as follow-ups, tracked: the pg-shaped redactor doesn't cover Snowflake Rebased onto current main (past the materialization tier); full gate green (typecheck, lint, prettier, unit + integration + skills). |
c705c03 to
813a65e
Compare
One conflict, in connection.controller.spec.ts, where both sides appended a new describe block at the end of the file: this branch's path-traversal name validation and main's getTable 404-not-502 mapping. Both kept. Signed-off-by: Nathan Huff <nuff@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… land The existence check ran against `cwd/../`, but since the throwaway config is rooted at an mkdtemp directory a regressed guard would write into tmpdir, so the assertion could never fail. Retarget it at the real path. Signed-off-by: Nathan Huff <nathan@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the DSN `SAFE_NAME_RE` admits `-`, so a name like `prod-lake` cleared both the controller and service guards and then failed to PARSE at the hyphen, putting the error position after the DSN literal. DuckDB renders a bounded window around that position, and at a range of DSN lengths the window opens between the scheme and the password. Every `redactPgSecrets` pass anchors on `scheme://user:`, so a window that clips the scheme redacts nothing and the password came back whole in `errorMessage`. Quote the alias, matching what the passthrough attach already did. Quoting the ducklake alias also makes hyphenated names attach for the first time, which newly exposes three identifier positions that fail soft -- both `set_option` CALLs and the format preflight -- so quote those too rather than leave a name that works while silently skipping its range check and size bounds. Signed-off-by: Nathan Huff <nathan@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… alias `attachedDb.name` is optional in the generated API type. The template literal accepted `undefined`; `quoteIdentifier` does not. Cast rather than guard, to keep the emitted SQL byte-identical for every config that attaches today. Signed-off-by: Nathan Huff <nathan@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the controller's copy Review findings from a cross-model pass. The hyphen sweep can only reach the parse error once the ducklake extension loads, so on a runner without it the test proved nothing. Add two pins that need no extension and no network: a stubbed-runSQL spec asserting the attached-database alias is quoted, and a hyphenated catalog alias through the existing federation stub. Both fail against the unquoted form; the sweep stays as defence in depth and now says so. `attachedDatabases[].name` carries a pattern in api-doc.yaml, but nothing validates requests against it, so a hyphen does reach the ATTACH. Also: the controller's own catch returned the driver text into `errorMessage` unredacted -- unreachable today, since the service resolves rather than throws, but it is the field this change exists to redact. And `quoteIdentifier` throws on an absent name where the old template produced `AS undefined`, so use the `|| ""` this function family already uses. Both probe files now carry a per-run suffix; the traversal probe's cleanup deletes unconditionally, and a fixed name could remove an unrelated file. Signed-off-by: Nathan Huff <nathan@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the ATTACH A second review round caught that the previous commit's `|| ""` was itself the bug it was meant to avoid. An absent alias emitted `AS ""`, and DuckDB answers that with `Parser Error: zero-length delimited identifier` positioned after the DSN literal -- the same truncation that strips the redactor's `scheme://user:` anchor and returns the password whole. Throwing before any SQL is built removes the statement, and with it the error that carried the DSN. The regression test asserts the guard's own message, not just the absence of cleartext: a short DSN redacts cleanly either way, so the not-contains check alone passed against the broken form and pinned nothing. Also preserve diagnostics for a non-Error throw in the controller, matching the service. The controller's catch stays untested by design. It is unreachable while the service resolves every failure rather than throwing, and reaching it needs a module mock -- which, since bun shares one process across spec files, breaks every test in connection.spec.ts. Signed-off-by: Nathan Huff <nathan@credibledata.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three merged behaviour changes reached main without a section: a breaking removal with a migration, a credential that a failed connection test returned in the clear, and a storage tier that served rows a source's filter excludes. The PR list alone would leave a reader upgrading unable to act on any of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: James Estes <james.estes@credibledata.com>
…1182) Three merged behaviour changes reached main without a section: a breaking removal with a migration, a credential that a failed connection test returned in the clear, and a storage tier that served rows a source's filter excludes. The PR list alone would leave a reader upgrading unable to act on any of them. Signed-off-by: James Estes <james.estes@credibledata.com> Co-authored-by: James Estes <james.estes@credibledata.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #915, delivering the two items committed to in the review reply, plus two fixes the specs and a security review forced into the open.
What this does
Redacts connection-test error messages.
testConnectionConfigreturned(error as Error).messageverbatim in thePOST /api/v0/connections/testresponse body. DuckDB attach failures echo the full connection string (IO Error: Unable to connect to Postgres at "postgres://user:pass@host/db"), so a failed test of a postgres/ducklake/duckdb-attached connection sent the cleartext password back to the API caller. The returnederrorMessagenow goes throughredactPgSecrets. The two log lines on the same paths (testConnectionConfig's catch andattachDatabasesToDuckDB's catch) printed the same DSN to the server log and are redacted the same way. The rethrow inattachDatabasesToDuckDBstays raw on purpose: redaction happens at boundaries (response bodies, log lines), andhandleAlreadyAttachedErrormatches on message content.Fixes connection testing for duckdb/ducklake connections, broken since Adopt MalloyConfig and scope DuckDB paths to package/project roots #682.
testConnectionConfigbuilt its throwaway config with an empty environment path, so DuckDB rejected the emptyworkingDirectorybefore any attach ran: every duckdb/ducklake connection test failed withworkingDirectory is invalid: path must not be emptyregardless of the config's validity. CI never noticed because the credential-gated specs skip without credentials and the unguarded invalid-config spec only asserts "failed with some message". The config is now rooted atprocess.cwd(), which is where the DuckLake cleanup in thefinallyalready expected the connection file to be (see the comment inducklake.test.ts). Without this fix, item 1 protects an unreachable path and cannot be tested.Guards the connection name against path traversal. Reaching a real attach (item 2) means the connection name now flows into
path.join(cwd,+ "${name}.duckdb" +), and here the name comes straight from the request body. A name like../foowould create a.duckdbfile outside the working directory.testConnectionConfignow runsassertSafePackageNameon the name before building the config, the same allowlist the DuckLake cleanup path (deleteDuckLakeConnectionFile) already applies to it, so any valid name is unaffected.Tightens the pass-2 comment in
pg_helpers.tsper @housejester's note 1 on fix(server): redact URL-form connection strings in redactPgSecrets #915: the raw-/mop-up only recovers the password when no raw@precedes the/. With both (postgres://u:p@a/b@h/d), pass 1's inserted***@satisfies the mop-up's first-@before it reaches the raw/, so the password tail after the raw@stays visible. The comment now says so, and a pinning spec documents the accepted residual.Tests
connection.spec.tsdrive real attach failures offline (localhost port 1, nothing can listen there unprivileged) through the URL-form, keyword-form, and ducklake-catalog shapes, asserting the returned message never contains the cleartext password (the unconditional security invariant) and, once the DSN has reached the message, that it is redacted. The positive check is conditional on the DSN being present so an environment where a DuckDB extension can't load fails honestly rather than on a missing marker.pg_helpers.spec.tsfor the pass-2 residual shape.Out of scope, known and left alone
buildPgConnectionStringunquoted-value handling (whitespace passwords),internalErrorToHttpError, and the MCP tool error paths: separate follow-ups from the fix(server): redact URL-form connection strings in redactPgSecrets #915 review.Connection test failed: ...) cannot carry a DSN today (the service catches internally; only cleanup errors reach it).<name>.duckdbfile in the server's cwd (pre-existing; only ducklake files are cleaned up). The new specs clean up their own.