Skip to content

Commit 683eb19

Browse files
committed
Enforce 1:1 baseDN-to-realm and remove cross-realm fallback
1 parent ad07d30 commit 683eb19

8 files changed

Lines changed: 297 additions & 627 deletions

File tree

docs/MULTI-REALM-ARCHITECTURE.md

Lines changed: 100 additions & 164 deletions
Large diffs are not rendered by default.

npm/src/LdapEngine.js

Lines changed: 111 additions & 197 deletions
Large diffs are not rendered by default.

npm/test/unit/LdapEngine.realms.test.js

Lines changed: 29 additions & 239 deletions
Large diffs are not rendered by default.

server/config/configurationLoader.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ class ConfigurationLoader {
127127
}
128128

129129
const names = new Set();
130+
const baseDns = new Map(); // lowercased baseDn -> realm name
131+
let defaultCount = 0;
130132
for (let i = 0; i < realms.length; i++) {
131133
const realm = realms[i];
132134
const prefix = `REALM_CONFIG[${i}]`;
@@ -148,6 +150,21 @@ class ConfigurationLoader {
148150
throw new Error(`${prefix} (${realm.name}): 'baseDn' is required and must be a string`);
149151
}
150152

153+
// Enforce 1:1 baseDN-to-realm mapping
154+
const baseDnKey = realm.baseDn.toLowerCase();
155+
if (baseDns.has(baseDnKey)) {
156+
throw new Error(
157+
`${prefix} (${realm.name}): duplicate baseDn '${realm.baseDn}' ` +
158+
`(already used by realm '${baseDns.get(baseDnKey)}'). ` +
159+
`Each baseDN must map to exactly one realm.`
160+
);
161+
}
162+
baseDns.set(baseDnKey, realm.name);
163+
164+
if (realm.default === true) {
165+
defaultCount++;
166+
}
167+
151168
if (!realm.directory || typeof realm.directory !== 'object') {
152169
throw new Error(`${prefix} (${realm.name}): 'directory' is required and must be an object`);
153170
}
@@ -175,7 +192,12 @@ class ConfigurationLoader {
175192
}
176193

177194
logger.info(`Realm '${realm.name}' configured with baseDN '${realm.baseDn}', ` +
178-
`directory: ${realm.directory.backend}, auth: [${realm.auth.backends.map(b => b.type).join(', ')}]`);
195+
`directory: ${realm.directory.backend}, auth: [${realm.auth.backends.map(b => b.type).join(', ')}]` +
196+
(realm.default ? ' (default)' : ''));
197+
}
198+
199+
if (defaultCount > 1) {
200+
throw new Error('REALM_CONFIG: only one realm may be marked as "default": true');
179201
}
180202
}
181203

server/realms.example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
{
33
"name": "example-corp",
44
"baseDn": "dc=example,dc=com",
5+
"default": true,
56
"directory": {
67
"backend": "sql",
78
"options": {

server/serverMain.js

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,11 @@ async function startServer(config) {
5252
requireAuthForSearch: config.requireAuthForSearch
5353
};
5454

55-
// Build global auth provider registry for per-user auth override (Phase 3)
56-
// Maps "realm:type" → AuthProvider instance (realm-scoped) and
57-
// "type" → AuthProvider instance (first-registered fallback)
58-
const authProviderRegistry = new Map();
59-
6055
if (config.realms) {
6156
// Multi-realm mode: build realm objects from config
6257
logger.info(`Initializing multi-realm mode with ${config.realms.length} realm(s)`);
58+
let defaultRealmObj = null;
59+
6360
engineOptions.realms = config.realms.map(realmCfg => {
6461
// Ensure directory providers receive realm-scoped LDAP base DN so that
6562
// any provider-side DN construction stays consistent with realmCfg.baseDn.
@@ -72,30 +69,40 @@ async function startServer(config) {
7269
directoryOptions
7370
);
7471

72+
// Build per-realm auth backend type map using explicit type names from config
73+
const authBackendTypes = new Map();
7574
const authProviders = realmCfg.auth.backends.map(backendCfg => {
7675
const provider = providerFactory.createAuthProvider(backendCfg.type, backendCfg.options || {});
77-
// Register with realm-scoped key for accurate per-user auth override
78-
const registryKey = `${realmCfg.name}:${backendCfg.type}`;
79-
authProviderRegistry.set(registryKey, provider);
80-
logger.debug(`Registered auth backend '${registryKey}' in provider registry`);
81-
// Also register type-only key as fallback (first realm wins per type)
82-
if (!authProviderRegistry.has(backendCfg.type)) {
83-
authProviderRegistry.set(backendCfg.type, provider);
84-
logger.debug(`Registered fallback auth backend '${backendCfg.type}' (first realm wins)`);
76+
const typeKey = backendCfg.type.toLowerCase();
77+
if (!authBackendTypes.has(typeKey)) {
78+
authBackendTypes.set(typeKey, provider);
8579
}
80+
logger.debug(`Realm '${realmCfg.name}': registered auth backend '${typeKey}'`);
8681
return provider;
8782
});
8883

8984
logger.info(`Realm '${realmCfg.name}': baseDN=${realmCfg.baseDn}, ` +
90-
`directory=${realmCfg.directory.backend}, auth=[${realmCfg.auth.backends.map(b => b.type).join(', ')}]`);
85+
`directory=${realmCfg.directory.backend}, auth=[${realmCfg.auth.backends.map(b => b.type).join(', ')}]` +
86+
(realmCfg.default ? ' (default)' : ''));
9187

92-
return {
88+
const realmObj = {
9389
name: realmCfg.name,
9490
baseDn: realmCfg.baseDn,
9591
directoryProvider,
96-
authProviders
92+
authProviders,
93+
authBackendTypes
9794
};
95+
96+
if (realmCfg.default) {
97+
defaultRealmObj = realmObj;
98+
}
99+
100+
return realmObj;
98101
});
102+
103+
if (defaultRealmObj) {
104+
engineOptions.defaultRealm = defaultRealmObj;
105+
}
99106
} else {
100107
// Legacy single-realm mode
101108
const selectedDirectory = providerFactory.createDirectoryProvider(config.directoryBackend);
@@ -106,18 +113,18 @@ async function startServer(config) {
106113
engineOptions.authProviders = selectedBackends;
107114
engineOptions.directoryProvider = selectedDirectory;
108115

109-
// Register legacy auth providers in the registry
116+
// Build auth backend types map for legacy mode
117+
const authBackendTypes = new Map();
110118
for (let idx = 0; idx < config.authBackends.length; idx++) {
111-
const backendType = config.authBackends[idx];
112-
if (!authProviderRegistry.has(backendType)) {
113-
authProviderRegistry.set(backendType, selectedBackends[idx]);
114-
logger.debug(`Registered auth backend '${backendType}' in provider registry`);
119+
const typeKey = config.authBackends[idx].toLowerCase();
120+
if (!authBackendTypes.has(typeKey)) {
121+
authBackendTypes.set(typeKey, selectedBackends[idx]);
122+
logger.debug(`Registered auth backend '${typeKey}' in provider type map`);
115123
}
116124
}
125+
engineOptions.authBackendTypes = authBackendTypes;
117126
}
118127

119-
engineOptions.authProviderRegistry = authProviderRegistry;
120-
121128
// Create and configure LDAP engine
122129
const ldapEngine = new LdapEngine(engineOptions);
123130

server/test/integration/auth/sqlite.auth.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function createClient() {
3434
return ldap.createClient({ url: `ldap://127.0.0.1:${port}` });
3535
}
3636

37-
// Minimal directory stub – only needs findUser so _authenticateAcrossRealms
37+
// Minimal directory stub – only needs findUser so _authenticateInRealm
3838
// can locate the user before delegating to the auth provider.
3939
const directoryStub = {
4040
initialize: async () => {},

server/test/unit/configurationLoader.realms.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,11 @@ describe('ConfigurationLoader._validateRealmConfig', () => {
183183
}])).toThrow("'auth.backends[0].type' is required");
184184
});
185185

186-
test('should accept multiple valid realms with shared baseDN', () => {
186+
test('should reject multiple realms with same baseDN', () => {
187187
const realms = [
188188
{ name: 'realm-a', baseDn: 'dc=shared,dc=com', directory: { backend: 'sql' }, auth: { backends: [{ type: 'sql' }] } },
189189
{ name: 'realm-b', baseDn: 'dc=shared,dc=com', directory: { backend: 'mongodb' }, auth: { backends: [{ type: 'mongodb' }] } }
190190
];
191-
expect(() => loader._validateRealmConfig(realms)).not.toThrow();
191+
expect(() => loader._validateRealmConfig(realms)).toThrow(/duplicate baseDn/);
192192
});
193193
});

0 commit comments

Comments
 (0)