Add support for multiple Base DNs in LDAP Identity Store (Fix #104)

This commit is contained in:
Max Dor
2018-12-22 22:51:09 +01:00
parent 06b2c787d3
commit e6f9c30611
9 changed files with 272 additions and 123 deletions

View File

@@ -139,6 +139,7 @@ dependencies {
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'com.github.tomakehurst:wiremock:2.8.0' testCompile 'com.github.tomakehurst:wiremock:2.8.0'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9'
} }
springBoot { springBoot {

View File

@@ -24,10 +24,13 @@ ldap.connection.host: 'ldapHostnameOrIp'
ldap.connection.port: 389 ldap.connection.port: 389
ldap.connection.bindDn: 'CN=My Mxisd User,OU=Users,DC=example,DC=org' ldap.connection.bindDn: 'CN=My Mxisd User,OU=Users,DC=example,DC=org'
ldap.connection.bindPassword: 'TheUserPassword' ldap.connection.bindPassword: 'TheUserPassword'
ldap.connection.baseDn: 'OU=Users,DC=example,DC=org' ldap.connection.baseDNs:
- 'OU=Users,DC=example,DC=org'
``` ```
These are standard LDAP connection configuration. mxisd will try to connect on port default port 389 without encryption. These are standard LDAP connection configuration. mxisd will try to connect on port default port 389 without encryption.
If you would like to use several Base DNs, simply add more entries under `baseDNs`.
### TLS/SSL connection ### TLS/SSL connection
If you would like to use a TLS/SSL connection, use the following configuration options (STARTLS not supported): If you would like to use a TLS/SSL connection, use the following configuration options (STARTLS not supported):
```yaml ```yaml

View File

@@ -30,6 +30,7 @@ import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult; import io.kamax.mxisd.auth.provider.BackendAuthResult;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ldap.LdapConfig; import io.kamax.mxisd.config.ldap.LdapConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.util.GsonUtil; import io.kamax.mxisd.util.GsonUtil;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException; import org.apache.directory.api.ldap.model.cursor.CursorException;
@@ -87,7 +88,6 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
public BackendAuthResult authenticate(_MatrixID mxid, String password) { public BackendAuthResult authenticate(_MatrixID mxid, String password) {
log.info("Performing auth for {}", mxid); log.info("Performing auth for {}", mxid);
try (LdapConnection conn = getConn()) { try (LdapConnection conn = getConn()) {
bind(conn); bind(conn);
@@ -108,11 +108,13 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
String[] attArray = new String[attributes.size()]; String[] attArray = new String[attributes.size()];
attributes.toArray(attArray); attributes.toArray(attArray);
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", userFilter); log.debug("Query: {}", userFilter);
log.debug("Attributes: {}", GsonUtil.build().toJson(attArray)); log.debug("Attributes: {}", GsonUtil.build().toJson(attArray));
try (EntryCursor cursor = conn.search(getBaseDn(), userFilter, SearchScope.SUBTREE, attArray)) { for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, userFilter, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get(); Entry entry = cursor.get();
String dn = entry.getDn().getName(); String dn = entry.getDn().getName();
@@ -159,11 +161,12 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
} catch (CursorLdapReferralException e) { } catch (CursorLdapReferralException e) {
log.warn("Entity for {} is only available via referral, skipping", mxid); log.warn("Entity for {} is only available via referral, skipping", mxid);
} }
}
log.info("No match were found for {}", mxid); log.info("No match were found for {}", mxid);
return BackendAuthResult.failure(); return BackendAuthResult.failure();
} catch (LdapException | IOException | CursorException e) { } catch (LdapException | IOException | CursorException e) {
throw new RuntimeException(e); throw new InternalServerError(e);
} }
} }

View File

@@ -59,8 +59,8 @@ public abstract class LdapBackend {
return cfg; return cfg;
} }
protected String getBaseDn() { protected List<String> getBaseDNs() {
return cfg.getConnection().getBaseDn(); return cfg.getConnection().getBaseDNs();
} }
protected LdapConfig.Attribute getAt() { protected LdapConfig.Attribute getAt() {

View File

@@ -65,18 +65,19 @@ public class LdapDirectoryProvider extends LdapBackend implements IDirectoryProv
bind(conn); bind(conn);
LdapConfig.Attribute atCfg = getCfg().getAttribute(); LdapConfig.Attribute atCfg = getCfg().getAttribute();
attributes = new ArrayList<>(attributes); attributes = new ArrayList<>(attributes);
attributes.add(getUidAtt()); attributes.add(getUidAtt());
String[] attArray = new String[attributes.size()]; String[] attArray = new String[attributes.size()];
attributes.toArray(attArray); attributes.toArray(attArray);
String searchQuery = buildOrQueryWithFilter(getCfg().getDirectory().getFilter(), "*" + query + "*", attArray); String searchQuery = buildOrQueryWithFilter(getCfg().getDirectory().getFilter(), "*" + query + "*", attArray);
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery); log.debug("Query: {}", searchQuery);
log.debug("Attributes: {}", GsonUtil.build().toJson(attArray)); log.debug("Attributes: {}", GsonUtil.build().toJson(attArray));
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, attArray)) { for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get(); Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName()); log.info("Found possible match, DN: {}", entry.getDn().getName());
@@ -93,6 +94,8 @@ public class LdapDirectoryProvider extends LdapBackend implements IDirectoryProv
}); });
} }
} }
}
} catch (CursorLdapReferralException e) { } catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping"); log.warn("An entry is only available via referral, skipping");
} catch (IOException | LdapException | CursorException e) { } catch (IOException | LdapException | CursorException e) {

View File

@@ -69,11 +69,11 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
bind(conn); bind(conn);
String searchQuery = buildOrQueryWithFilter(getCfg().getProfile().getFilter(), uid, getUidAtt()); String searchQuery = buildOrQueryWithFilter(getCfg().getProfile().getFilter(), uid, getUidAtt());
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery); log.debug("Query: {}", searchQuery);
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, getAt().getName())) { for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, getAt().getName())) {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get(); Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName()); log.info("Found possible match, DN: {}", entry.getDn().getName());
@@ -92,9 +92,10 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
return v; return v;
} }
} }
}
} catch (CursorLdapReferralException e) { } catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping"); log.warn("An entry is only available via referral, skipping");
}
}
} catch (IOException | LdapException | CursorException e) { } catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e); throw new InternalServerError(e);
} }
@@ -111,7 +112,6 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
try (LdapConnection conn = getConn()) { try (LdapConnection conn = getConn()) {
bind(conn); bind(conn);
log.debug("Base DN: {}", getBaseDn());
getCfg().getAttribute().getThreepid().forEach((medium, attributes) -> { getCfg().getAttribute().getThreepid().forEach((medium, attributes) -> {
String[] attArray = new String[attributes.size()]; String[] attArray = new String[attributes.size()];
attributes.toArray(attArray); attributes.toArray(attArray);
@@ -120,7 +120,9 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
log.debug("Query for 3PID {}: {}", medium, searchQuery); log.debug("Query for 3PID {}: {}", medium, searchQuery);
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, attArray)) { for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get(); Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName()); log.info("Found possible match, DN: {}", entry.getDn().getName());
@@ -137,11 +139,11 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
} }
} catch (CursorLdapReferralException e) { } catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping"); log.warn("An entry is only available via referral, skipping");
} catch (IOException | LdapException | CursorException e) { } catch (LdapException | IOException | CursorException e) {
throw new InternalServerError(e); throw new InternalServerError(e);
} }
}
}); });
} catch (IOException | LdapException e) { } catch (IOException | LdapException e) {
throw new InternalServerError(e); throw new InternalServerError(e);
} }

View File

@@ -78,12 +78,13 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
// we merge 3PID specific query with global/specific filter, if one exists. // we merge 3PID specific query with global/specific filter, if one exists.
String tPidQuery = tPidQueryOpt.get().replaceAll(getCfg().getIdentity().getToken(), value); String tPidQuery = tPidQueryOpt.get().replaceAll(getCfg().getIdentity().getToken(), value);
String searchQuery = buildWithFilter(tPidQuery, getCfg().getIdentity().getFilter()); String searchQuery = buildWithFilter(tPidQuery, getCfg().getIdentity().getFilter());
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery); log.debug("Query: {}", searchQuery);
log.debug("Attributes: {}", GsonUtil.build().toJson(getUidAtt())); log.debug("Attributes: {}", GsonUtil.build().toJson(getUidAtt()));
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, getUidAtt())) { for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, getUidAtt())) {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get(); Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName()); log.info("Found possible match, DN: {}", entry.getDn().getName());
@@ -101,6 +102,7 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
} catch (IOException | LdapException | CursorException e) { } catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e); throw new InternalServerError(e);
} }
}
return Optional.empty(); return Optional.empty();
} }

View File

@@ -110,6 +110,7 @@ public abstract class LdapConfig {
private String bindDn; private String bindDn;
private String bindPassword; private String bindPassword;
private String baseDn; private String baseDn;
private List<String> baseDNs = new ArrayList<>();
public boolean isTls() { public boolean isTls() {
return tls; return tls;
@@ -151,14 +152,24 @@ public abstract class LdapConfig {
this.bindPassword = bindPassword; this.bindPassword = bindPassword;
} }
@Deprecated
public String getBaseDn() { public String getBaseDn() {
return baseDn; return baseDn;
} }
@Deprecated
public void setBaseDn(String baseDn) { public void setBaseDn(String baseDn) {
this.baseDn = baseDn; this.baseDn = baseDn;
} }
public List<String> getBaseDNs() {
return baseDNs;
}
public void setBaseDNs(List<String> baseDNs) {
this.baseDNs = baseDNs;
}
} }
public static class Directory { public static class Directory {
@@ -253,11 +264,11 @@ public abstract class LdapConfig {
private boolean enabled; private boolean enabled;
private String filter; private String filter;
private Connection connection; private Connection connection = new Connection();
private Attribute attribute; private Attribute attribute = new Attribute();
private Auth auth; private Auth auth = new Auth();
private Directory directory; private Directory directory = new Directory();
private Identity identity; private Identity identity = new Identity();
private Profile profile = new Profile(); private Profile profile = new Profile();
protected abstract String getConfigName(); protected abstract String getConfigName();
@@ -343,8 +354,14 @@ public abstract class LdapConfig {
throw new IllegalStateException("LDAP port is not valid"); throw new IllegalStateException("LDAP port is not valid");
} }
if (StringUtils.isBlank(connection.getBaseDn())) { // Backward compatibility with the old option
throw new ConfigurationException("ldap.connection.baseDn"); if (!StringUtils.isBlank(connection.baseDn)) {
connection.getBaseDNs().add(connection.baseDn);
}
if (connection.getBaseDNs().isEmpty()) {
throw new ConfigurationException("ldap.connection.baseDNs",
"You must specify at least one Base DN via the singular or plural config option");
} }
if (StringUtils.isBlank(attribute.getUid().getType())) { if (StringUtils.isBlank(attribute.getUid().getType())) {
@@ -386,7 +403,10 @@ public abstract class LdapConfig {
log.info("Port: {}", connection.getPort()); log.info("Port: {}", connection.getPort());
log.info("TLS: {}", connection.isTls()); log.info("TLS: {}", connection.isTls());
log.info("Bind DN: {}", connection.getBindDn()); log.info("Bind DN: {}", connection.getBindDn());
log.info("Base DN: {}", connection.getBaseDn()); log.info("Base DNs: {}");
for (String baseDN : connection.getBaseDNs()) {
log.info("\t- {}", baseDN);
}
log.info("Attribute: {}", GsonUtil.get().toJson(attribute)); log.info("Attribute: {}", GsonUtil.get().toJson(attribute));
log.info("Auth: {}", GsonUtil.get().toJson(auth)); log.info("Auth: {}", GsonUtil.get().toJson(auth));

View File

@@ -0,0 +1,115 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.test.backend.ldap;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.LDAPException;
import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import io.kamax.mxisd.backend.ldap.LdapAuthProvider;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ldap.LdapConfig;
import io.kamax.mxisd.config.ldap.generic.GenericLdapConfig;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.ArrayList;
import static org.junit.Assert.assertFalse;
public class LdapAuthTest {
private static InMemoryDirectoryServer ds;
private static ArrayList<String> dnList = new ArrayList<>();
@BeforeClass
public static void beforeClass() throws LDAPException {
dnList.add("dc=1,dc=mxisd,dc=example,dc=org");
dnList.add("dc=2,dc=mxisd,dc=example,dc=org");
dnList.add("dc=3,dc=mxisd,dc=example,dc=org");
InMemoryListenerConfig lCfg = InMemoryListenerConfig.createLDAPConfig("localhost", 65001);
InMemoryDirectoryServerConfig config =
new InMemoryDirectoryServerConfig(dnList.get(0), dnList.get(1), dnList.get(2));
config.addAdditionalBindCredentials("cn=mxisd", "mxisd");
config.setListenerConfigs(lCfg);
ds = new InMemoryDirectoryServer(config);
ds.startListening();
}
@AfterClass
public static void afterClass() {
ds.shutDown(true);
}
@Test
public void singleDn() {
MatrixConfig mxCfg = new MatrixConfig();
mxCfg.setDomain("example.org");
mxCfg.build();
LdapConfig cfg = new GenericLdapConfig();
cfg.getConnection().setHost("localhost");
cfg.getConnection().setPort(65001);
cfg.getConnection().setBaseDn(dnList.get(0));
cfg.getConnection().setBindDn("cn=mxisd");
cfg.getConnection().setBindPassword("mxisd");
LdapConfig.UID uid = new LdapConfig.UID();
uid.setType("uid");
uid.setValue("saMAccountName");
cfg.getAttribute().setUid(uid);
cfg.build();
LdapAuthProvider p = new LdapAuthProvider(cfg, mxCfg);
BackendAuthResult result = p.authenticate(MatrixID.from("john", "example.org").valid(), "doe");
assertFalse(result.isSuccess());
}
@Test
public void multiDNs() {
MatrixConfig mxCfg = new MatrixConfig();
mxCfg.setDomain("example.org");
mxCfg.build();
LdapConfig cfg = new GenericLdapConfig();
cfg.getConnection().setHost("localhost");
cfg.getConnection().setPort(65001);
cfg.getConnection().setBaseDNs(dnList);
cfg.getConnection().setBindDn("cn=mxisd");
cfg.getConnection().setBindPassword("mxisd");
LdapConfig.UID uid = new LdapConfig.UID();
uid.setType("uid");
uid.setValue("saMAccountName");
cfg.getAttribute().setUid(uid);
cfg.build();
LdapAuthProvider p = new LdapAuthProvider(cfg, mxCfg);
BackendAuthResult result = p.authenticate(MatrixID.from("john", "example.org").valid(), "doe");
assertFalse(result.isSuccess());
}
}