Skip to content

Fix LDAP base DN and entryUUID handling for hosted directories - #2546

Draft
leandro-lorenzini wants to merge 4 commits into
warp-tech:mainfrom
leandro-lorenzini:fix/ldap-base-dns-persistence
Draft

Fix LDAP base DN and entryUUID handling for hosted directories#2546
leandro-lorenzini wants to merge 4 commits into
warp-tech:mainfrom
leandro-lorenzini:fix/ldap-base-dns-persistence

Conversation

@leandro-lorenzini

@leandro-lorenzini leandro-lorenzini commented Sep 4, 2026

Copy link
Copy Markdown

Description

Three related defects that make Warpgate's LDAP integration unusable against hosted multi-tenant directories. All were hit configuring JumpCloud Cloud LDAP; each is reproducible on stock OpenLDAP too, and the third affects any directory that doesn't publish a searchable naming context.

The common thread: base DNs and UUIDs are treated as things the server discovers, never as things an operator can state.

1. Base DNs are overwritten on every save

api_update_ldap_server called discover_base_dns unconditionally and assigned over model.base_dns, and UpdateLdapServerRequest had no base_dns field. Base DNs simply could not be set — the RootDSE won every save, and the UI showed them read-only as "Base DNs (discovered)".

JumpCloud advertises dc=jumpcloud,dc=com and ou=service-accounts as naming contexts. Neither is searchable by a tenant:

LDAP query failed: Search failed in dc=jumpcloud,dc=com:
LDAP operation result: rc=32 (noSuchObject), dn: "", text: ""

The tenant's real base is ou=Users,o=<org id>,dc=jumpcloud,dc=com, and the org id appears only in the bind DN — discovery cannot derive it. The only workaround was UPDATE ldap_servers SET base_dns = ..., which the next save silently undid.

Fix: optional base_dns on the update request, honoured when it holds at least one non-blank entry, discovery otherwise — so clearing the list still re-detects. The UI list becomes editable (same pattern as AllowedIpRangesEditor) and round-trips on save.

2. A string-valued entryUUID is never read

With uuid_attribute unset, extract_ldap_user looked only in bin_attrs:

search_entry.bin_attrs.get("objectGUID")
    .or_else(|| search_entry.bin_attrs.get("entryUUID"))

AD returns objectGUID as 16 raw bytes, so that path works. entryUUID is a dashed string, which ldap3 decodes as UTF-8 into attrs — so the lookup never matches and extraction fails with NoUUID, despite the comment claiming OpenLDAP is covered.

find_user_by_filter turns that into a warning and Ok(None), so the caller reports no user found for an entry the directory did return. Auto-link failed with No LDAP user found with username: <name> while an identical ldapsearch returned the entry — a confusing place to land, since the message points at the username rather than at UUID parsing.

Fix: fall back to parsing entryUUID from attrs, mirroring what the configured-attribute branch already does.

3. A server whose RootDSE is unusable can't be created

api_create_ldap_server propagated discovery failure with ?, so registration failed before the row existed — leaving nothing to correct on the detail page.

Fix: accept the same optional base_dns and skip discovery when provided. Unchanged when omitted.

Compatibility

Additive. Both new fields are optional and omitting them preserves today's behaviour, so existing clients and the generated SDKs are unaffected.

Testing

Diagnosed against a live JumpCloud tenant; the base DN reverting on every save is what led to the code. Confirmed ldapsearch with the explicit base returns users that discovery's DNs cannot, and that JumpCloud serves entryUUID as a string (94708f40-…), which is what surfaced defect 2.

Verified locally on the pinned nightly-2026-07-09:

Check Result
cargo check -p warpgate-ldap -p warpgate-admin clean
cargo cranky --workspace --all-features (what just clippy runs) exit 0, no diagnostics in either changed file
biome ci on the changed Svelte file (2.5.9, as pinned) clean

openapi-schema.json was edited by hand first, then checked by regenerating it with cargo run -p warpgate-admin and diffing: the output is byte-identical apart from the version field, which tracks the build's git describe. I kept the hand-edited file so that field isn't churned.

The workspace cranky run does emit 11 warnings, all pre-existing and in other crates — four manifest-level unused-dependency notes, a map_or simplification and nine other lints in warpgate-protocol-http, and a future-incompat note from proc-macro-error2. None are introduced here.

Deliberately not included

The create page still relies on discovery — base DNs stay editable afterwards on the detail page, and adding a second editor there seemed like scope creep. Happy to add it if you'd rather the forms match.

AI Usage

Choose the level of AI involvement for this PR.

  • Fully vibe coded
  • AI-designed, AI-coded, manually checked
  • Human-designed, AI-coded
  • Human-designed, human-coded (includes AI autocompletions and boilerplate gen)

This is not to block AI contributions but rather to speed up PR review (saves time on trying to deduce the logic behind AI hallucinations).

All three defects were hit during a real JumpCloud deployment and confirmed against the source and a live directory before patching; the code itself is AI-written, then compiled and linted with the project's own toolchain and lint config as described above.

`api_update_ldap_server` ran `discover_base_dns` unconditionally and
assigned the result over `model.base_dns`, and `UpdateLdapServerRequest`
had no `base_dns` field at all. Base DNs therefore could not be set
through the admin UI: whatever the RootDSE advertised replaced them on
every save, and the UI only rendered them as a read-only
"Base DNs (discovered)" list.

That is fine for a directory whose RootDSE names a searchable base, but
not for hosted multi-tenant LDAP. JumpCloud, for example, advertises
`dc=jumpcloud,dc=com` and `ou=service-accounts` as naming contexts;
neither is searchable by a tenant, and searching them fails with
`rc=32 (noSuchObject)`. The tenant's real base
(`ou=Users,o=<org id>,dc=jumpcloud,dc=com`) is derivable only from the
bind DN, so discovery cannot produce it and the operator has no way to
supply it. The only workaround was editing the database directly.

Add an optional `base_dns` to the update request and honour it when it
contains at least one non-blank entry, falling back to discovery
otherwise, so clearing the list still re-detects. Make the UI field an
editable list that round-trips the value on save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@leandro-lorenzini
leandro-lorenzini marked this pull request as draft September 4, 2026 08:43
leandro-lorenzini and others added 2 commits September 4, 2026 16:44
With `uuid_attribute` unset, `extract_ldap_user` only looked in
`bin_attrs`:

```rust
search_entry
    .bin_attrs
    .get("objectGUID")
    .or_else(|| search_entry.bin_attrs.get("entryUUID"))
```

Active Directory returns `objectGUID` as 16 raw bytes, so that works.
`entryUUID` is a dashed string, which ldap3 decodes as UTF-8 into
`attrs`, so the lookup never matches and extraction fails with
`NoUUID` — despite the comment saying OpenLDAP is covered.

`find_user_by_filter` logs that as a warning and returns `Ok(None)`, so
the caller reports "no user found" for an entry the directory did
return. Against JumpCloud, which serves `entryUUID` as a string, this
made auto-link fail with `No LDAP user found with username: <name>`
while the identical `ldapsearch` returned the entry.

Fall back to parsing `entryUUID` from `attrs`, mirroring what the
configured-attribute branch above already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`api_create_ldap_server` always ran discovery and propagated its error
with `?`, so a directory that does not advertise a usable naming
context could not be registered at all — the request failed before the
row was written, leaving nothing to correct on the detail page.

Accept the same optional `base_dns` the update endpoint now takes and
skip discovery when it is provided, so callers that already know their
search base are not blocked by the RootDSE. Behaviour is unchanged when
the field is omitted.

The create page itself still relies on discovery; base DNs remain
editable afterwards on the server's detail page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@leandro-lorenzini leandro-lorenzini changed the title Stop overwriting LDAP base DNs on every save Fix LDAP base DN and entryUUID handling for hosted directories Sep 4, 2026
`a_string_entry_uuid_is_read_when_no_uuid_attribute_is_configured` fails
on the previous `bin_attrs`-only lookup and passes with the fallback, so
it pins the regression rather than just describing it. The other three
guard the paths that already worked: a binary `objectGUID`, a configured
attribute holding a string, and an entry with no UUID at all still being
rejected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant