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 'com.github.tomakehurst:wiremock:2.8.0'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9'
}
springBoot {

View File

@@ -24,10 +24,13 @@ ldap.connection.host: 'ldapHostnameOrIp'
ldap.connection.port: 389
ldap.connection.bindDn: 'CN=My Mxisd User,OU=Users,DC=example,DC=org'
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.
If you would like to use several Base DNs, simply add more entries under `baseDNs`.
### TLS/SSL connection
If you would like to use a TLS/SSL connection, use the following configuration options (STARTLS not supported):
```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.config.MatrixConfig;
import io.kamax.mxisd.config.ldap.LdapConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.util.GsonUtil;
import org.apache.commons.lang.StringUtils;
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) {
log.info("Performing auth for {}", mxid);
try (LdapConnection conn = getConn()) {
bind(conn);
@@ -108,62 +108,65 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
String[] attArray = new String[attributes.size()];
attributes.toArray(attArray);
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", userFilter);
log.debug("Attributes: {}", GsonUtil.build().toJson(attArray));
try (EntryCursor cursor = conn.search(getBaseDn(), userFilter, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
String dn = entry.getDn().getName();
log.info("Checking possible match, DN: {}", dn);
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
if (!getAttribute(entry, getUidAtt()).isPresent()) {
continue;
}
try (EntryCursor cursor = conn.search(baseDN, userFilter, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
String dn = entry.getDn().getName();
log.info("Checking possible match, DN: {}", dn);
log.info("Attempting authentication on LDAP for {}", dn);
try {
conn.bind(entry.getDn(), password);
} catch (LdapException e) {
log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage());
return BackendAuthResult.failure();
}
if (!getAttribute(entry, getUidAtt()).isPresent()) {
continue;
}
Attribute nameAttribute = entry.get(getAt().getName());
String name = nameAttribute != null ? nameAttribute.get().toString() : null;
log.info("Attempting authentication on LDAP for {}", dn);
try {
conn.bind(entry.getDn(), password);
} catch (LdapException e) {
log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage());
return BackendAuthResult.failure();
}
log.info("Authentication successful for {}", entry.getDn().getName());
log.info("DN {} is a valid match", dn);
Attribute nameAttribute = entry.get(getAt().getName());
String name = nameAttribute != null ? nameAttribute.get().toString() : null;
// TODO should we canonicalize the MXID?
BackendAuthResult result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, name);
log.info("Processing 3PIDs for profile");
getAt().getThreepid().forEach((k, v) -> {
log.info("Processing 3PID type {}", k);
v.forEach(attId -> {
List<String> values = getAttributes(entry, attId);
log.info("\tAttribute {} has {} value(s)", attId, values.size());
getAttributes(entry, attId).forEach(tpidValue -> {
if (ThreePidMedium.PhoneNumber.is(k)) {
tpidValue = getMsisdn(tpidValue).orElse(tpidValue);
}
result.withThreePid(new ThreePid(k, tpidValue));
log.info("Authentication successful for {}", entry.getDn().getName());
log.info("DN {} is a valid match", dn);
// TODO should we canonicalize the MXID?
BackendAuthResult result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, name);
log.info("Processing 3PIDs for profile");
getAt().getThreepid().forEach((k, v) -> {
log.info("Processing 3PID type {}", k);
v.forEach(attId -> {
List<String> values = getAttributes(entry, attId);
log.info("\tAttribute {} has {} value(s)", attId, values.size());
getAttributes(entry, attId).forEach(tpidValue -> {
if (ThreePidMedium.PhoneNumber.is(k)) {
tpidValue = getMsisdn(tpidValue).orElse(tpidValue);
}
result.withThreePid(new ThreePid(k, tpidValue));
});
});
});
});
log.info("Found {} 3PIDs", result.getProfile().getThreePids().size());
return result;
log.info("Found {} 3PIDs", result.getProfile().getThreePids().size());
return result;
}
} catch (CursorLdapReferralException e) {
log.warn("Entity for {} is only available via referral, skipping", mxid);
}
} catch (CursorLdapReferralException e) {
log.warn("Entity for {} is only available via referral, skipping", mxid);
}
log.info("No match were found for {}", mxid);
return BackendAuthResult.failure();
} 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;
}
protected String getBaseDn() {
return cfg.getConnection().getBaseDn();
protected List<String> getBaseDNs() {
return cfg.getConnection().getBaseDNs();
}
protected LdapConfig.Attribute getAt() {

View File

@@ -65,34 +65,37 @@ public class LdapDirectoryProvider extends LdapBackend implements IDirectoryProv
bind(conn);
LdapConfig.Attribute atCfg = getCfg().getAttribute();
attributes = new ArrayList<>(attributes);
attributes.add(getUidAtt());
String[] attArray = new String[attributes.size()];
attributes.toArray(attArray);
String searchQuery = buildOrQueryWithFilter(getCfg().getDirectory().getFilter(), "*" + query + "*", attArray);
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery);
log.debug("Attributes: {}", GsonUtil.build().toJson(attArray));
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
getAttribute(entry, getUidAtt()).ifPresent(uid -> {
log.info("DN {} is a valid match", entry.getDn().getName());
try {
UserDirectorySearchResult.Result entryResult = new UserDirectorySearchResult.Result();
entryResult.setUserId(buildMatrixIdFromUid(uid));
getAttribute(entry, atCfg.getName()).ifPresent(entryResult::setDisplayName);
result.addResult(entryResult);
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", atCfg.getUid().getType());
}
});
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
getAttribute(entry, getUidAtt()).ifPresent(uid -> {
log.info("DN {} is a valid match", entry.getDn().getName());
try {
UserDirectorySearchResult.Result entryResult = new UserDirectorySearchResult.Result();
entryResult.setUserId(buildMatrixIdFromUid(uid));
getAttribute(entry, atCfg.getName()).ifPresent(entryResult::setDisplayName);
result.addResult(entryResult);
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", atCfg.getUid().getType());
}
});
}
}
}
} catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping");
} catch (IOException | LdapException | CursorException e) {

View File

@@ -69,32 +69,33 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
bind(conn);
String searchQuery = buildOrQueryWithFilter(getCfg().getProfile().getFilter(), uid, getUidAtt());
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery);
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, getAt().getName())) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Optional<String> v = getAttribute(entry, getAt().getName()).flatMap(id -> {
log.info("DN {} is a valid match", entry.getDn().getName());
try {
return getAttribute(entry, getAt().getName());
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", getAt().getUid().getType());
return Optional.empty();
}
});
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, getAt().getName())) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Optional<String> v = getAttribute(entry, getAt().getName()).flatMap(id -> {
log.info("DN {} is a valid match", entry.getDn().getName());
try {
return getAttribute(entry, getAt().getName());
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", getAt().getUid().getType());
return Optional.empty();
}
});
if (v.isPresent()) {
log.info("DN {} is the final match", entry.getDn().getName());
return v;
if (v.isPresent()) {
log.info("DN {} is the final match", entry.getDn().getName());
return v;
}
}
} catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping");
}
}
} catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping");
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
@@ -111,7 +112,6 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
try (LdapConnection conn = getConn()) {
bind(conn);
log.debug("Base DN: {}", getBaseDn());
getCfg().getAttribute().getThreepid().forEach((medium, attributes) -> {
String[] attArray = new String[attributes.size()];
attributes.toArray(attArray);
@@ -120,28 +120,30 @@ public class LdapProfileProvider extends LdapBackend implements ProfileProvider
log.debug("Query for 3PID {}: {}", medium, searchQuery);
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
try {
attributes.stream()
.flatMap(at -> getAttributes(entry, at).stream())
.forEach(address -> {
log.info("Found 3PID: {} - {}", medium, address);
threePids.add(new ThreePid(medium, address));
});
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", getAt().getUid().getType());
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, attArray)) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
try {
attributes.stream()
.flatMap(at -> getAttributes(entry, at).stream())
.forEach(address -> {
log.info("Found 3PID: {} - {}", medium, address);
threePids.add(new ThreePid(medium, address));
});
} catch (IllegalArgumentException e) {
log.warn("Bind was found but type {} is not supported", getAt().getUid().getType());
}
}
} catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping");
} catch (LdapException | IOException | CursorException e) {
throw new InternalServerError(e);
}
} catch (CursorLdapReferralException e) {
log.warn("An entry is only available via referral, skipping");
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
});
} catch (IOException | LdapException e) {
throw new InternalServerError(e);
}

View File

@@ -78,28 +78,30 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
// we merge 3PID specific query with global/specific filter, if one exists.
String tPidQuery = tPidQueryOpt.get().replaceAll(getCfg().getIdentity().getToken(), value);
String searchQuery = buildWithFilter(tPidQuery, getCfg().getIdentity().getFilter());
log.debug("Base DN: {}", getBaseDn());
log.debug("Query: {}", searchQuery);
log.debug("Attributes: {}", GsonUtil.build().toJson(getUidAtt()));
try (EntryCursor cursor = conn.search(getBaseDn(), searchQuery, SearchScope.SUBTREE, getUidAtt())) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
for (String baseDN : getBaseDNs()) {
log.debug("Base DN: {}", baseDN);
Optional<String> data = getAttribute(entry, getUidAtt());
if (!data.isPresent()) {
continue;
try (EntryCursor cursor = conn.search(baseDN, searchQuery, SearchScope.SUBTREE, getUidAtt())) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Optional<String> data = getAttribute(entry, getUidAtt());
if (!data.isPresent()) {
continue;
}
log.info("DN {} is a valid match", entry.getDn().getName());
return Optional.of(buildMatrixIdFromUid(data.get()));
}
log.info("DN {} is a valid match", entry.getDn().getName());
return Optional.of(buildMatrixIdFromUid(data.get()));
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
} catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
return Optional.empty();

View File

@@ -110,6 +110,7 @@ public abstract class LdapConfig {
private String bindDn;
private String bindPassword;
private String baseDn;
private List<String> baseDNs = new ArrayList<>();
public boolean isTls() {
return tls;
@@ -151,14 +152,24 @@ public abstract class LdapConfig {
this.bindPassword = bindPassword;
}
@Deprecated
public String getBaseDn() {
return baseDn;
}
@Deprecated
public void setBaseDn(String baseDn) {
this.baseDn = baseDn;
}
public List<String> getBaseDNs() {
return baseDNs;
}
public void setBaseDNs(List<String> baseDNs) {
this.baseDNs = baseDNs;
}
}
public static class Directory {
@@ -253,11 +264,11 @@ public abstract class LdapConfig {
private boolean enabled;
private String filter;
private Connection connection;
private Attribute attribute;
private Auth auth;
private Directory directory;
private Identity identity;
private Connection connection = new Connection();
private Attribute attribute = new Attribute();
private Auth auth = new Auth();
private Directory directory = new Directory();
private Identity identity = new Identity();
private Profile profile = new Profile();
protected abstract String getConfigName();
@@ -343,8 +354,14 @@ public abstract class LdapConfig {
throw new IllegalStateException("LDAP port is not valid");
}
if (StringUtils.isBlank(connection.getBaseDn())) {
throw new ConfigurationException("ldap.connection.baseDn");
// Backward compatibility with the old option
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())) {
@@ -386,7 +403,10 @@ public abstract class LdapConfig {
log.info("Port: {}", connection.getPort());
log.info("TLS: {}", connection.isTls());
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("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());
}
}