Commit Graph
7 Commits
Author SHA1 Message Date
ogrechkoandClaude Sonnet 5 b8ca595d34 Shell out to ldapsearch for the directory browse instead of ldapjs
Bisecting through attribute selection and paging kept reproducing the
same failure in different shapes ("Encoding too long" BER parser error,
then ECONNRESET) — all while a plain `ldapsearch` against the exact same
query, run from inside this same container, completed successfully every
single time. That's strong enough evidence of a bug somewhere in
ldapjs/@ldapjs-asn1's BER decoding against this AD's actual response
bytes, not in our query. Rather than keep chasing a third-party parser
bug, searchLdapDirectory() now shells out to the system `ldapsearch`
(added to the image via ldap-utils) and parses its LDIF output directly
— the same tool that's already proven reliable here. authenticateLdapUser
(the per-login lookup) is untouched: it's a narrow single-match query
that has shown no sign of this issue, and isn't worth the added latency
of spawning a process on every login.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 11:41:26 +00:00
ogrechkoandClaude Sonnet 5 cb20d9eea5 Drop RFC 2696 paging now that the response is attribute-limited
Paging was introduced to work around what looked like a response-size
problem, but the actual cause (previous commit) was @ldapjs/asn1's BER
decoder choking on a specific oversized attribute value, not overall
response size. With attributes now limited to mail/cn/displayName, the
whole response is tiny, and paging's extra sequential round-trips on the
same connection were themselves causing an ECONNRESET partway through.
An unpaged single request+response now matches what a plain ldapsearch
against this same AD does successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 11:32:21 +00:00
ogrechkoandClaude Sonnet 5 1e8133ee39 Request only mail/cn/displayName from LDAP instead of every attribute
The real error surfaced by the previous commit's diagnostics: "Parser
error ... Encoding too long" from @ldapjs/asn1's BER decoder — a parser
desync (it misreads some later byte as a new field's length prefix),
most likely triggered while decoding some AD attribute value on one of
the ~1000 objects that we never actually use (e.g. nTSecurityDescriptor,
msDS-ReplAttributeMetaData, or similar oversized/binary attributes AD
attaches to many objects by default). Every call site only ever reads
mail/cn/displayName, so there was never a reason to request the full
attribute set — doing so avoids decoding whatever was tripping the bug,
regardless of exactly which attribute it was.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 11:18:57 +00:00
ogrechkoandClaude Sonnet 5 2a505da975 Surface ldapjs's real parser/error cause instead of a generic "closed"
Neither raising the client timeout nor switching to paged search fixed
the directory-wide import browse — narrowing it down with a plain
ldapsearch (same filter, run from inside this exact container, both
dn-only and full-attribute) showed the network path and response
content are both fine; a standard LDAP client handles it without issue.
That leaves ldapjs's own response handling. Its client-level `error`
event (e.g. a BER/protocol parser failure) forcibly closes the socket,
but we were swallowing that event silently — so every failure surfaced
as the same uninformative "<id> closed" ConnectionError regardless of
what actually went wrong. Now the last captured client `error` is
attached to the bind/search rejection instead, so the UI's error
message will show the real underlying cause next time this fails.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 11:04:22 +00:00
ogrechkoandClaude Sonnet 5 a40723b61b Use paged (RFC 2696) LDAP search for the directory-wide import browse
A plain ldapsearch against this AD with the identical broad filter
(objectClass=person) completed cleanly — 996 entries, exit 0 — ruling
out the network/server as the cause of the "<id> closed" ConnectionError
our app was hitting. The difference is response size: ~1000 LDAP protocol
messages in one unbounded response is apparently enough to trip up
ldapjs's client-side handling, even though our own busy_timeout increase
(previous commit) didn't help since the connection was being closed, not
timed out. Paging the search (200 entries per round-trip, handled
internally by ldapjs — callers still just see one continuous stream of
searchEntry events) sidesteps the issue regardless of its exact root
cause. Applies to both the admin "import accounts" browse and the
per-login user lookup, since they share the same search() helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 10:45:52 +00:00
ogrechkoandClaude Sonnet 5 f9128499df Raise LDAP client timeout for the directory-wide import browse
ldapjs's `timeout` option is a single flat deadline for the whole
operation (set once when the request is sent, not reset per entry
received) — 5s is fine for a bind or a single-match search during login,
but the admin "import accounts" browse runs an unbounded
(objectClass=person) search across the entire base DN, which can take
longer than 5s against a real AD domain and was getting killed mid-stream
(surfaced as ldapjs's generic "<id> closed" ConnectionError). Bumped that
one call site to 30s; login-path calls keep the tighter 5s deadline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012o9j9RezxbZVKQMrB7oRLY
2026-08-05 10:19:39 +00:00
ogrechkoandClaude Sonnet 5 cb12711e91 Add account management and LDAP login
Admins can now create local agent/admin accounts and configure LDAP
directly from the UI (Настройки → Аккаунты / LDAP), with login trying
LDAP first and falling back to the local password. This is the first
place users.role is actually enforced (requireAdminSession).

Fixed a bug in the generated 0005 migration: the INSERT into __new_users
selected auth_source from the old users table, which doesn't have that
column yet — caused drizzle-kit migrate to fail silently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcXH24ky6zjk2UyK5oZUPH
2026-07-27 08:56:08 +00:00