Conversation
authentication chain on a per-user basis.
There was a problem hiding this comment.
Pull request overview
Adds multi-realm support to the LDAP gateway so a single server can route binds/searches across multiple realm configurations (baseDN + directory backend + auth chain), with provider options override support for per-realm configuration.
Changes:
- Introduces multi-realm routing in
@ldap-gateway/core(LdapEngine) for bind/search/RootDSE plus per-user auth-chain override support. - Updates server configuration loading and provider instantiation to accept per-realm options (and adds realm config examples/tests).
- Refactors SQL backend Sequelize option building into a shared utility and updates backends/examples/tests for options-based configuration.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| server/utils/sqlUtils.js | New shared Sequelize options helper used by SQL providers. |
| server/test/unit/configurationLoader.realms.test.js | Unit tests for REALM_CONFIG loading/validation. |
| server/test/integration/auth/sqlite.auth.test.js | Adjusts auth integration test to satisfy multi-realm bind flow (directory stub + client cleanup). |
| server/test/integration/auth/postgres.auth.test.js | Uses directory stub to satisfy multi-realm bind flow. |
| server/test/integration/auth/mysql.auth.test.js | Uses directory stub to satisfy multi-realm bind flow. |
| server/services/notificationService.js | Allows overriding notification URL (used by notification auth backend options). |
| server/serverMain.js | Builds engine options for legacy vs multi-realm mode; constructs realms and auth provider registry. |
| server/realms.example.json | Example multi-realm configuration file. |
| server/providers.js | ProviderFactory now passes options into backend constructors. |
| server/config/configurationLoader.js | Loads/validates REALM_CONFIG (inline JSON or file path). |
| server/backends/template.js | Updates backend template to use options with env fallback. |
| server/backends/sql.directory.js | SQL directory provider now accepts options (sqlUri/queries/baseDn) and uses shared Sequelize options helper. |
| server/backends/sql.auth.js | SQL auth provider now accepts options (sqlUri/query) and uses shared Sequelize options helper. |
| server/backends/proxmox.directory.js | Proxmox directory provider now accepts options (paths/baseDn) and uses options-based DN building. |
| server/backends/proxmox.auth.js | Proxmox auth provider now accepts options (shadow cfg + optional directoryProvider). |
| server/backends/notification.auth.js | Notification auth provider now accepts options (notificationUrl) and passes it through. |
| server/backends/mongodb.directory.js | Mongo directory provider now accepts options (uri/db/baseDn). |
| server/backends/mongodb.auth.js | Mongo auth provider now accepts options (uri/db). |
| server/backends/ldap.auth.js | LDAP auth backend now supports options override for bind/auth base settings. |
| server/backends/custom-directory.example.js | Example directory backend updated to options + env fallback. |
| server/backends/custom-auth.example.js | Example auth backend updated to options + env fallback. |
| npm/test/unit/LdapEngine.realms.test.js | New unit tests covering multi-realm behavior, RootDSE namingContexts, and per-user auth override. |
| npm/test/fixtures/testData.js | Adds fixture user with auth_backends field for override tests. |
| npm/test/fixtures/mockProviders.js | Mock providers updated to call super(options). |
| npm/src/LdapEngine.js | Core multi-realm bind/search/RootDSE support and per-user auth override resolution. |
| npm/src/DirectoryProvider.js | Base class now stores constructor options. |
| npm/src/AuthProvider.js | Base class now stores constructor options. |
| nfpm/scripts/postinstall.sh | Fixes postinstall conditional syntax. |
| docker/sql/init.sql | Adds auth_backends column, fixes FK, and updates seeded passwords to bcrypt hashes. |
…to feature/multi-realm
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…to feature/multi-realm
npm/src/LdapEngine.js
Outdated
| } else { | ||
| this.logger.warn( | ||
| `User '${username}' found in multiple realms: '${matchedRealm.name}' and '${realm.name}'. ` + | ||
| `Using first match '${matchedRealm.name}'.` |
There was a problem hiding this comment.
This is wrong. There's no reason the same username couldn't be present in multiple realms. By looking them up by username at bind time, there's a potential auth bypass. Assume we've got example.com and example.org as realms. alice is a user in bother of them. In example.com we enforce 2FA but in example.org we don't. alice attempts to authenticate to a server with SSSD configured to use example.com but this finds her in the example.org realm and never presents the 2FA challenge.
If I'm not mistaken, the bind request in LDAP already requires the full DN of the user. We should instead parse their realm based on their DN components rather than looking them up from their uid.
There was a problem hiding this comment.
DN-based realm routing is already handled correctly. In _setupBindHandlers(), we call this.server.bind(baseDn, handler) once per unique baseDN. Each handler is scoped to only the realms associated with that specific baseDN.
For example, if we configure realms for dc=example,dc=com and dc=example,dc=org, a bind request for uid=alice,dc=example,dc=com is routed exclusively to the dc=example,dc=com handler. It never reaches the dc=example,dc=org handler or its authentication chain.
Additionally, the "multiple realms" loop in _authenticateAcrossRealms only iterates over realms that share the same baseDN, not across all configured realms. The associated warning would only trigger if multiple realms are configured with an identical baseDN, a scenario where the DN itself provides no way to differentiate between them anyway.
flowchart TB
subgraph "Client Layer"
C1["SSSD Client<br/>uid=alice,dc=mieweb,dc=com"]
C2["SSSD Client<br/>uid=bob,dc=bluehive,dc=com"]
C3["SSSD Client<br/>uid=carol,dc=mieweb,dc=org"]
end
subgraph "LDAP Gateway"
GW["ldapjs BaseDN Router<br/>server.bind(baseDn, handler)"]
end
subgraph "Handler: dc=mieweb,dc=com"
R1["Realm: mieweb"]
R1 --> AUTH1["Auth: SQL + Notification"]
end
subgraph "Handler: dc=bluehive,dc=com"
R2["Realm: bluehive"]
R2 --> AUTH2["Auth: SQL"]
end
subgraph "Handler: dc=mieweb,dc=org"
R3["Realm: mieweb-org"]
R3 --> AUTH3["Auth: Proxmox"]
end
C1 -->|"suffix matches"| GW
C2 -->|"suffix matches"| GW
C3 -->|"suffix matches"| GW
GW -->|"dc=mieweb,dc=com"| R1
GW -->|"dc=bluehive,dc=com"| R2
GW -->|"dc=mieweb,dc=org"| R3
flowchart TB
subgraph "Client Layer"
C1["SSSD Client<br/>uid=apant,dc=mieweb,dc=com"]
C2["CI Pipeline<br/>uid=ci-bot,dc=mieweb,dc=com"]
end
subgraph "LDAP Gateway"
GW["ldapjs routes by baseDN suffix"]
end
subgraph "Handler: dc=mieweb,dc=com"
LOOP["Iterate realms in config order"]
subgraph "Realm 1: employees"
DIR1["SQL Directory<br/>SELECT * FROM users"]
AUTH1["Auth: SQL + MFA"]
end
subgraph "Realm 2: service-accounts"
DIR2["SQL Directory<br/>SELECT * FROM service_accounts"]
AUTH2["Auth: SQL only"]
end
LOOP -->|"1. findUser()"| DIR1
DIR1 -->|"not found"| DIR2
DIR1 -->|"found"| AUTH1
LOOP -->|"2. findUser()"| DIR2
DIR2 -->|"found"| AUTH2
end
C1 -->|"bind"| GW
C2 -->|"bind"| GW
GW -->|"dc=mieweb,dc=com"| LOOP
There was a problem hiding this comment.
I don't think it should be implemented where one baseDN can have multiple matching realms. Without enforcement that ie. alice is only present in one of them, there's a lot that can go wrong. Essentially, I think baseDN and realm should map one-to-one.
npm/src/LdapEngine.js
Outdated
| // Build a quick lookup map of realm's own auth providers by type | ||
| const realmAuthByType = new Map(); | ||
| for (const provider of realm.authProviders) { | ||
| // Extract type from constructor name (e.g., SQLAuthProvider -> sql) |
There was a problem hiding this comment.
Why are we doing this instead of the Type and Name from the module export? This seems really fragile.
npm/src/LdapEngine.js
Outdated
| const normalizedName = name.toLowerCase(); | ||
|
|
||
| // 1. Check realm's own auth providers first | ||
| let provider = realmAuthByType.get(normalizedName); |
There was a problem hiding this comment.
I think if this fails we shouldn't have any other fallbacks. Could lead to authentication bypass by admin misconfiguration. If the backend can't be resolved within the realm, we should immediately throw an error and cause authentication to fail.
There was a problem hiding this comment.
Agreed. I'll simplify it to only resolve from the realm's own providers and throw immediately if a backend isn't found there.
npm/src/LdapEngine.js
Outdated
| scope: req.scope | ||
| }); | ||
|
|
||
| entryCount = await this._handleMultiRealmSearch(realms, filterStr, req.attributes, res); |
There was a problem hiding this comment.
Why are we searching all realms in a search handler that is scoped per realm? If I'm not mistaken this means ie. uid=alice,dc=example,dc=com could show up even if my search's base is dc=example,dc=org?
There was a problem hiding this comment.
npm/src/LdapEngine.js
Outdated
| const attrLower = attr.toLowerCase(); | ||
| if (attrLower === 'namingcontexts') { | ||
| attributes.namingContexts = [this.config.baseDn]; | ||
| attributes.namingContexts = allBaseDns; |
There was a problem hiding this comment.
This breaks how I'm using this for SSSD discovery1:
Default: If not set, the value of the defaultNamingContext or namingContexts attribute from the RootDSE of the LDAP server is used. If defaultNamingContext does not exist or has an empty value namingContexts is used. The namingContexts attribute must have a single value with the DN of the search base of the LDAP server to make this work. Multiple values are are not supported.
Can we add a default setting to realms that, if set, renders that realm as the defaultNamingContext to ensure if more than one realm is set I can still specify a default so SSSD discovery will work?
Summary
Adds multi-realm architecture to the LDAP Gateway. A single gateway instance can now serve multiple directory backends, each with its own baseDN and authentication chain, while remaining fully backward compatible with existing single-realm deployments.
What Changed
Core Engine - LdapEngine now supports multiple realms, each with independent directory and auth providers. LDAP operations are routed by baseDN. Binds find the user across realms (first match wins) and authenticate against that realm. Searches query all matching realms in parallel and merge results with DN deduplication.
Per-User Auth Override - Users can override their realm default auth chain via an
auth_backendsdatabase column. Backend names are resolved case-insensitively through a three-level registry (realm providers, realm-scoped key, global fallback). Unknown backends fail loudly.Provider Options Passthrough - All backends (SQL, MongoDB, Proxmox, LDAP, Notification) now accept constructor options with env var fallback, enabling per-realm configuration without environment variable conflicts.
Configuration - New
REALM_CONFIGenv var points to arealms.jsonfile (or inline JSON). Includes validation and structured error messages. When not set, legacy env vars work exactly as before.Server Wiring - serverMain.js builds realm objects from config, instantiates per-realm providers, and populates the auth provider registry.
Backward Compatibility
When
REALM_CONFIGis not set, the gateway behaves identically to before. Legacy env vars (AUTH_BACKENDS,DIRECTORY_BACKEND,LDAP_BASE_DN) are auto-wrapped into a single realm named "default". No client or configuration changes required for existing deployments.Testing
Files Changed
npm/src/LdapEngine.js- Multi-realm engine with baseDN routingserver/serverMain.js- Realm instantiation and registry buildingserver/config/configurationLoader.js- REALM_CONFIG loading and validationserver/backends/*.js- Options passthrough for all providersdocker/sql/init.sql- auth_backends columndocs/MULTI-REALM-ARCHITECTURE.md- Feature documentationserver/realms.example.json- Example configuration