Fix LDAP directory import: paginate past server size limits, stop leaking bind password into error messages
The "import accounts" browse was shelling out a single unbounded ldapsearch call, which failed outright with "Size limit exceeded (4)" once the directory grew past whatever sizeLimit the LDAP server enforces for this bind account. Added RFC 2696 paged results (-E pr=500/noprompt) so ldapsearch transparently walks the whole base DN in pages instead of one request that trips the limit. Separately: execFile's rejection .message is "Command failed: <full argv>\n<stderr>", and the full argv includes the bind password passed via -w — that was reaching the admin UI verbatim in the error banner shown after a failed search. Only stderr (ldapsearch's own diagnostic text, which never echoes its invocation) is now surfaced. Also fixes an unrelated image-build flake: drizzle-kit's migrate step leaves db.sqlite in SQLite's default (non-WAL) journal mode, and next build's parallel page-data-collection workers then race to perform the first-ever journal_mode=WAL switch — a mode change that, unlike ordinary statements, doesn't reliably honor busy_timeout, so one worker's pragma call throws SQLITE_BUSY regardless of the configured timeout. Switching to WAL once, single-process, right after migration avoids the race entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gh2UXUQUVBWroWEnn1FLFG
This commit is contained in:
1 parent
d54107d722
commit
93e17c37c7
2 files changed
+29
No files matched your search
+10
@@ -22,6 +22,16 @@ COPY . .
|
||||
# Migrating first — as its own isolated, single-process step — means the
|
||||
# workers only ever see an already-existing, already-stable file.
|
||||
RUN mkdir -p data && npm run db:migrate
|
||||
# A second, separate race: drizzle-kit's migrate command opens its own
|
||||
# connection and leaves the file in SQLite's default (non-WAL) journal
|
||||
# mode. The *first* pragma("journal_mode = WAL") call on a file is a real
|
||||
# mode switch requiring a brief exclusive lock — and unlike ordinary
|
||||
# statements, that specific switch doesn't reliably honor busy_timeout, so
|
||||
# when all of next build's workers race to be the one performing it, one
|
||||
# loses and throws SQLITE_BUSY regardless of the 30s timeout configured in
|
||||
# lib/db/client.ts. Switching to WAL here, once, single-process, means
|
||||
# every worker's own pragma call below is just a no-op mode check.
|
||||
RUN node -e "require('better-sqlite3')('data/db.sqlite').pragma('journal_mode = WAL')"
|
||||
RUN npm run build
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -119,11 +119,20 @@ async function ldapSearchCli(
|
||||
filter: string,
|
||||
): Promise<{ dn: string; attributes: Record<string, string> }[]> {
|
||||
const protocol = settings.useTls ? "ldaps" : "ldap";
|
||||
try {
|
||||
const { stdout } = await execFileAsync(
|
||||
"ldapsearch",
|
||||
[
|
||||
"-x",
|
||||
"-LLL",
|
||||
// Simple Paged Results (RFC 2696): without it, a base DN with more
|
||||
// entries than the server's admin-configured sizeLimit (AD default:
|
||||
// 1000, but often set lower) fails the whole search outright —
|
||||
// "Size limit exceeded (4)" — even though every individual page is
|
||||
// under the limit. ldapsearch transparently walks all pages and
|
||||
// concatenates the LDIF output, so parseLdif below needs no changes.
|
||||
"-E",
|
||||
"pr=500/noprompt",
|
||||
"-H",
|
||||
`${protocol}://${settings.host}:${settings.port}`,
|
||||
"-D",
|
||||
@@ -143,6 +152,16 @@ async function ldapSearchCli(
|
||||
{ maxBuffer: 20 * 1024 * 1024, timeout: 15_000 },
|
||||
);
|
||||
return parseLdif(stdout);
|
||||
} catch (err) {
|
||||
// execFile's rejection .message is "Command failed: <full argv>\n<stderr>"
|
||||
// — the full argv includes -w <bindPassword> verbatim, which would
|
||||
// otherwise reach callers (and, via the admin API's error responses,
|
||||
// the browser) in plain text. Only stderr — ldapsearch's own diagnostic
|
||||
// text, which never echoes the arguments it was invoked with — is safe
|
||||
// to surface.
|
||||
const stderr = (err as { stderr?: string })?.stderr?.trim();
|
||||
throw new Error(stderr || "ldapsearch failed");
|
||||
}
|
||||
}
|
||||
|
||||
/** Escapes an untrusted value for safe interpolation into an LDAP search filter (RFC 4515). */
|
||||
|
||||
Reference in new issue
Block a user