Skip to content

Feature/multi realm#143

Open
anishapant21 wants to merge 21 commits intodevfrom
feature/multi-realm
Open

Feature/multi realm#143
anishapant21 wants to merge 21 commits intodevfrom
feature/multi-realm

Conversation

@anishapant21
Copy link
Collaborator

@anishapant21 anishapant21 commented Mar 2, 2026

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_backends database 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_CONFIG env var points to a realms.json file (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_CONFIG is 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

  • 931 lines of new unit tests for multi-realm routing, authentication, search, per-user override, and backward compatibility
  • 193 lines of new unit tests for realm config loading and validation

Files Changed

  • npm/src/LdapEngine.js - Multi-realm engine with baseDN routing
  • server/serverMain.js - Realm instantiation and registry building
  • server/config/configurationLoader.js - REALM_CONFIG loading and validation
  • server/backends/*.js - Options passthrough for all providers
  • docker/sql/init.sql - auth_backends column
  • docs/MULTI-REALM-ARCHITECTURE.md - Feature documentation
  • server/realms.example.json - Example configuration

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.

anishapant21 and others added 5 commits March 15, 2026 18:16
@anishapant21 anishapant21 marked this pull request as ready for review March 16, 2026 14:24
} else {
this.logger.warn(
`User '${username}' found in multiple realms: '${matchedRealm.name}' and '${realm.name}'. ` +
`Using first match '${matchedRealm.name}'.`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

@anishapant21 anishapant21 Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
    
Loading
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
Loading

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing this instead of the Type and Name from the module export? This seems really fragile.

const normalizedName = name.toLowerCase();

// 1. Check realm's own auth providers first
let provider = realmAuthByType.get(normalizedName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'll simplify it to only resolve from the realm's own providers and throw immediately if a backend isn't found there.

scope: req.scope
});

entryCount = await this._handleMultiRealmSearch(realms, filterStr, req.attributes, res);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const attrLower = attr.toLowerCase();
if (attrLower === 'namingcontexts') {
attributes.namingContexts = [this.config.baseDn];
attributes.namingContexts = allBaseDns;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Footnotes

  1. https://linux.die.net/man/5/sssd-ldap

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.

3 participants