You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/04/27 07:25:22 UTC
[james-project] branch master updated: JAMES-3905 LDAP should allow per user base DN (#1540)
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 8768378814 JAMES-3905 LDAP should allow per user base DN (#1540)
8768378814 is described below
commit 8768378814b7cffdf20934164b181b45b5740867
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Thu Apr 27 14:25:17 2023 +0700
JAMES-3905 LDAP should allow per user base DN (#1540)
---
.../ROOT/pages/configure/usersrepository.adoc | 24 ++++++++--
.../user/ldap/LdapRepositoryConfiguration.java | 45 ++++++++++++++++--
.../james/user/ldap/ReadOnlyLDAPUsersDAO.java | 54 ++++++++++++++++------
.../user/ldap/ReadOnlyUsersLDAPRepositoryTest.java | 46 ++++++++++++++++++
.../src/test/resources/ldif-files/Dockerfile | 2 +-
.../src/test/resources/ldif-files/populate.ldif | 13 ++++++
src/site/xdoc/server/config-users.xml | 25 ++++++++--
7 files changed, 180 insertions(+), 29 deletions(-)
diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/usersrepository.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/usersrepository.adoc
index 21adc49072..e9020c6ac3 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/usersrepository.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/usersrepository.adoc
@@ -60,8 +60,10 @@ to get some examples and hints.
Example:
....
-<repository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldap://myldapserver:389"
- principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"/>;
+<usersrepository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldap://myldapserver:389"
+ principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid">
+ <enableVirtualHosting>true</enableVirtualHosting>
+</usersrepository>
....
SSL can be enabled by using `ldaps` scheme. `trustAllCerts` option can be used to trust all LDAP client certificates
@@ -70,7 +72,21 @@ SSL can be enabled by using `ldaps` scheme. `trustAllCerts` option can be used t
Example:
....
-<repository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldaps://myldapserver:636"
+<usersrepository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldaps://myldapserver:636"
principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"
- trustAllCerts="true"/>;
+ trustAllCerts="true">
+ <enableVirtualHosting>true</enableVirtualHosting>
+</usersrepository>
+....
+
+Moreover, per domain base DN can be configured:
+
+....
+<usersrepository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldap://myldapserver:389"
+ principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"
+ <enableVirtualHosting>true</enableVirtualHosting>
+ <domains>
+ <domain.tld>ou=People,o=other.com,ou=system</domain.tld>
+ </domains>
+</usersrepository>
....
diff --git a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/LdapRepositoryConfiguration.java b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/LdapRepositoryConfiguration.java
index 1fb7f38017..e9e0b50fb8 100644
--- a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/LdapRepositoryConfiguration.java
+++ b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/LdapRepositoryConfiguration.java
@@ -19,15 +19,18 @@
package org.apache.james.user.ldap;
+import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Domain;
import org.apache.james.core.Username;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
public class LdapRepositoryConfiguration {
public static final String SUPPORTS_VIRTUAL_HOSTING = "supportsVirtualHosting";
@@ -49,6 +52,7 @@ public class LdapRepositoryConfiguration {
private Optional<String> userObjectClass;
private Optional<Integer> poolSize;
private Optional<Boolean> trustAllCerts;
+ private ImmutableMap.Builder<Domain, String> perDomainBaseDN;
public Builder() {
ldapHost = Optional.empty();
@@ -59,6 +63,7 @@ public class LdapRepositoryConfiguration {
userObjectClass = Optional.empty();
poolSize = Optional.empty();
trustAllCerts = Optional.empty();
+ perDomainBaseDN = ImmutableMap.builder();
}
public Builder ldapHost(String ldapHost) {
@@ -101,6 +106,11 @@ public class LdapRepositoryConfiguration {
return this;
}
+ public Builder addPerDomainDN(Domain domain, String dn) {
+ this.perDomainBaseDN.put(domain, dn);
+ return this;
+ }
+
public LdapRepositoryConfiguration build() throws ConfigurationException {
Preconditions.checkState(ldapHost.isPresent(), "'ldapHost' is mandatory");
Preconditions.checkState(principal.isPresent(), "'principal' is mandatory");
@@ -123,7 +133,8 @@ public class LdapRepositoryConfiguration {
NO_RESTRICTION,
NO_FILTER,
NO_ADMINISTRATOR_ID,
- trustAllCerts.orElse(false));
+ trustAllCerts.orElse(false),
+ perDomainBaseDN.build());
}
}
@@ -159,6 +170,19 @@ public class LdapRepositoryConfiguration {
int poolSize = Optional.ofNullable(configuration.getInteger("[@poolSize]", null))
.orElse(DEFAULT_POOL_SIZE);
+ ImmutableMap.Builder<Domain, String> builder = ImmutableMap.builder();
+ if (configuration.getNodeModel()
+ .getInMemoryRepresentation()
+ .getChildren()
+ .stream()
+ .anyMatch(n -> n.getNodeName().equals("domains"))) {
+ HierarchicalConfiguration<ImmutableNode> domains = configuration.configurationAt("domains");
+ Iterator<String> keys = domains.getKeys();
+ while (keys.hasNext()) {
+ String next = keys.next();
+ builder.put(Domain.of(next), domains.getString(next));
+ }
+ }
return new LdapRepositoryConfiguration(
ldapHost,
principal,
@@ -173,7 +197,8 @@ public class LdapRepositoryConfiguration {
restriction,
filter,
administratorId,
- trustAllCerts);
+ trustAllCerts,
+ builder.build());
}
/**
@@ -250,12 +275,16 @@ public class LdapRepositoryConfiguration {
* The administrator is allowed to log in as other users
*/
private final Optional<Username> administratorId;
+
private final boolean trustAllCerts;
+ private final ImmutableMap<Domain, String> perDomainBaseDN;
+
private LdapRepositoryConfiguration(String ldapHost, String principal, String credentials, String userBase, String userIdAttribute,
String userObjectClass, int connectionTimeout, int readTimeout,
boolean supportsVirtualHosting, int poolSize, ReadOnlyLDAPGroupRestriction restriction, String filter,
- Optional<String> administratorId, boolean trustAllCerts) throws ConfigurationException {
+ Optional<String> administratorId, boolean trustAllCerts,
+ ImmutableMap<Domain, String> perDomainBaseDN) throws ConfigurationException {
this.ldapHost = ldapHost;
this.principal = principal;
this.credentials = credentials;
@@ -270,6 +299,7 @@ public class LdapRepositoryConfiguration {
this.filter = filter;
this.administratorId = administratorId.map(Username::of);
this.trustAllCerts = trustAllCerts;
+ this.perDomainBaseDN = perDomainBaseDN;
checkState();
}
@@ -342,6 +372,10 @@ public class LdapRepositoryConfiguration {
return trustAllCerts;
}
+ public ImmutableMap<Domain, String> getPerDomainBaseDN() {
+ return perDomainBaseDN;
+ }
+
@Override
public final boolean equals(Object o) {
if (o instanceof LdapRepositoryConfiguration) {
@@ -360,7 +394,8 @@ public class LdapRepositoryConfiguration {
&& Objects.equals(this.filter, that.filter)
&& Objects.equals(this.poolSize, that.poolSize)
&& Objects.equals(this.administratorId, that.administratorId)
- && Objects.equals(this.trustAllCerts, that.trustAllCerts);
+ && Objects.equals(this.trustAllCerts, that.trustAllCerts)
+ && Objects.equals(this.perDomainBaseDN, that.perDomainBaseDN);
}
return false;
}
@@ -369,6 +404,6 @@ public class LdapRepositoryConfiguration {
public final int hashCode() {
return Objects.hash(ldapHost, principal, credentials, userBase, userIdAttribute, userObjectClass,
connectionTimeout, readTimeout, supportsVirtualHosting, restriction, filter, administratorId, poolSize,
- trustAllCerts);
+ trustAllCerts, perDomainBaseDN);
}
}
diff --git a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
index 083441dfc6..ef4c0bbcec 100644
--- a/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
+++ b/server/data/data-ldap/src/main/java/org/apache/james/user/ldap/ReadOnlyLDAPUsersDAO.java
@@ -43,6 +43,7 @@ import javax.net.ssl.X509TrustManager;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Domain;
import org.apache.james.core.Username;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.user.api.UsersRepositoryException;
@@ -52,6 +53,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.fge.lambdas.Throwing;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DN;
@@ -61,6 +63,7 @@ import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPConnectionPool;
import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
@@ -146,6 +149,10 @@ public class ReadOnlyLDAPUsersDAO implements UsersDAO, Configurable {
objectClassFilter = Filter.createEqualityFilter("objectClass", ldapConfiguration.getUserObjectClass());
listingFilter = userExtraFilter.map(extraFilter -> Filter.createANDFilter(objectClassFilter, extraFilter))
.orElse(objectClassFilter);
+
+ if (!ldapConfiguration.getPerDomainBaseDN().isEmpty()) {
+ Preconditions.checkState(ldapConfiguration.supportsVirtualHosting(), "'virtualHosting' is needed for per domain DNs");
+ }
}
private SocketFactory supportLDAPS(URI uri) throws KeyManagementException, NoSuchAlgorithmException {
@@ -205,30 +212,46 @@ public class ReadOnlyLDAPUsersDAO implements UsersDAO, Configurable {
return result;
}
- private Set<DN> getAllUsersDNFromLDAP() throws LDAPException {
- SearchRequest searchRequest = new SearchRequest(ldapConfiguration.getUserBase(),
- SearchScope.SUB,
- listingFilter,
- SearchRequest.NO_ATTRIBUTES);
+ private String userBase(Domain domain) {
+ return ldapConfiguration.getPerDomainBaseDN()
+ .getOrDefault(domain, ldapConfiguration.getUserBase());
+ }
- SearchResult searchResult = ldapConnectionPool.search(searchRequest);
+ private String userBase(Username username) {
+ return username.getDomainPart().map(this::userBase).orElse(ldapConfiguration.getUserBase());
+ }
- return searchResult.getSearchEntries()
- .stream()
+ private Set<DN> getAllUsersDNFromLDAP() throws LDAPException {
+ return allDNs()
+ .flatMap(Throwing.<String, Stream<SearchResultEntry>>function(this::entriesFromDN).sneakyThrow())
.map(Throwing.function(Entry::getParsedDN))
.collect(ImmutableSet.toImmutableSet());
}
- private Stream<Username> getAllUsernamesFromLDAP() throws LDAPException {
- SearchRequest searchRequest = new SearchRequest(ldapConfiguration.getUserBase(),
+ private Stream<String> allDNs() {
+ return Stream.concat(
+ Stream.of(ldapConfiguration.getUserBase()),
+ ldapConfiguration.getPerDomainBaseDN().values().stream());
+ }
+
+ private Stream<SearchResultEntry> entriesFromDN(String dn) throws LDAPSearchException {
+ return entriesFromDN(dn, SearchRequest.NO_ATTRIBUTES);
+ }
+
+ private Stream<SearchResultEntry> entriesFromDN(String dn, String attributes) throws LDAPSearchException {
+ SearchRequest searchRequest = new SearchRequest(dn,
SearchScope.SUB,
listingFilter,
- ldapConfiguration.getUserIdAttribute());
+ attributes);
- SearchResult searchResult = ldapConnectionPool.search(searchRequest);
+ return ldapConnectionPool.search(searchRequest)
+ .getSearchEntries()
+ .stream();
+ }
- return searchResult.getSearchEntries()
- .stream()
+ private Stream<Username> getAllUsernamesFromLDAP() throws LDAPException {
+ return allDNs()
+ .flatMap(Throwing.<String, Stream<SearchResultEntry>>function(s -> entriesFromDN(s, ldapConfiguration.getUserIdAttribute())).sneakyThrow())
.flatMap(entry -> Optional.ofNullable(entry.getAttribute(ldapConfiguration.getUserIdAttribute())).stream())
.map(Attribute::getValue)
.map(Username::of);
@@ -260,7 +283,7 @@ public class ReadOnlyLDAPUsersDAO implements UsersDAO, Configurable {
}
private Optional<ReadOnlyLDAPUser> searchAndBuildUser(Username name) throws LDAPException {
- SearchResult searchResult = ldapConnectionPool.search(ldapConfiguration.getUserBase(),
+ SearchResult searchResult = ldapConnectionPool.search(userBase(name),
SearchScope.SUB,
createFilter(name.asString()),
ldapConfiguration.getUserIdAttribute());
@@ -269,6 +292,7 @@ public class ReadOnlyLDAPUsersDAO implements UsersDAO, Configurable {
.stream()
.findFirst()
.orElse(null);
+
if (result == null) {
return Optional.empty();
}
diff --git a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
index 646c588f13..28c31d6610 100644
--- a/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
+++ b/server/data/data-ldap/src/test/java/org/apache/james/user/ldap/ReadOnlyUsersLDAPRepositoryTest.java
@@ -89,6 +89,52 @@ class ReadOnlyUsersLDAPRepositoryTest {
.isInstanceOf(LDAPException.class);
}
+ @Nested
+ class ExtraDNTests {
+ private ReadOnlyUsersLDAPRepository usersLDAPRepository;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ HierarchicalConfiguration<ImmutableNode> configuration = ldapRepositoryConfigurationWithVirtualHosting(ldapContainer);
+ configuration.addProperty("domains.extra.org", "ou=whatever,dc=james,dc=org");
+
+ usersLDAPRepository = new ReadOnlyUsersLDAPRepository(new SimpleDomainList());
+ usersLDAPRepository.configure(configuration);
+ usersLDAPRepository.init();
+ }
+
+ @Test
+ void shouldContainMasterDomain() throws Exception {
+ assertThat(usersLDAPRepository.contains(JAMES_USER_MAIL)).isTrue();
+ }
+
+ @Test
+ void shouldRejectUnhandledDomain() throws Exception {
+ assertThat(usersLDAPRepository.contains(Username.of("bob@nonexistant.org"))).isFalse();
+ }
+
+ @Test
+ void shouldContainEntriesInExtraDN() throws Exception {
+ assertThat(usersLDAPRepository.contains(Username.of("bob@extra.org"))).isTrue();
+
+ assertThat(usersLDAPRepository.countUsers()).isEqualTo(2);
+
+ assertThat(ImmutableList.copyOf(usersLDAPRepository.list()))
+ .containsOnly(JAMES_USER_MAIL, Username.of("bob@extra.org"));
+ }
+
+ @Test
+ void shouldCountUsersInBothOrgs() throws Exception {
+ assertThat(usersLDAPRepository.countUsers()).isEqualTo(2);
+ }
+
+ @Test
+ void shouldListUsersInBothOrgs() throws Exception {
+ assertThat(ImmutableList.copyOf(usersLDAPRepository.list()))
+ .containsOnly(JAMES_USER_MAIL, Username.of("bob@extra.org"));
+ }
+ }
+
@Nested
class FilterTests {
@Test
diff --git a/server/data/data-ldap/src/test/resources/ldif-files/Dockerfile b/server/data/data-ldap/src/test/resources/ldif-files/Dockerfile
index d889a35fb7..7e3b0b473f 100644
--- a/server/data/data-ldap/src/test/resources/ldif-files/Dockerfile
+++ b/server/data/data-ldap/src/test/resources/ldif-files/Dockerfile
@@ -1,3 +1,3 @@
FROM dinkel/openldap:latest
-COPY populate.ldif /etc/ldap/prepopulate/prepop.ldif
+COPY populate.ldif /etc/ldap.dist/prepopulate/prepop.ldif
diff --git a/server/data/data-ldap/src/test/resources/ldif-files/populate.ldif b/server/data/data-ldap/src/test/resources/ldif-files/populate.ldif
index 586125d46c..8e373b03f4 100644
--- a/server/data/data-ldap/src/test/resources/ldif-files/populate.ldif
+++ b/server/data/data-ldap/src/test/resources/ldif-files/populate.ldif
@@ -6,6 +6,10 @@ dn: ou=empty, dc=james,dc=org
ou: empty
objectClass: organizationalUnit
+dn: ou=whatever, dc=james,dc=org
+ou: whatever
+objectClass: organizationalUnit
+
dn: uid=james-user, ou=people, dc=james,dc=org
objectClass: inetOrgPerson
uid: james-user
@@ -14,3 +18,12 @@ sn: james-user
mail: james-user@james.org
userPassword: secret
description: James user
+
+dn: uid=bob, ou=whatever, dc=james,dc=org
+objectClass: inetOrgPerson
+uid: bob
+cn: bob
+sn: bob
+mail: bob@extra.org
+userPassword: secret
+description: bob user
diff --git a/src/site/xdoc/server/config-users.xml b/src/site/xdoc/server/config-users.xml
index ddc780aca5..b6ad980472 100644
--- a/src/site/xdoc/server/config-users.xml
+++ b/src/site/xdoc/server/config-users.xml
@@ -94,19 +94,36 @@
<p>Example:</p>
<source>
-<repository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldap://myldapserver:389"
- principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"/></source>
+<usersrepository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldap://myldapserver:389"
+ principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid">
+ <enableVirtualHosting>true</enableVirtualHosting>
+</usersrepository>
+ </source>
<p>SSL can be enabled by using <code>ldaps</code> scheme. <code>trustAllCerts</code> option can be used to trust all LDAP client certificates
(optional, defaults to false).</p>
- Example:
+ <p>Example:</p>
<source>
<repository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldaps://myldapserver:636"
principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"
- trustAllCerts="true"/></source>
+ trustAllCerts="true">
+ <enableVirtualHosting>true</enableVirtualHosting>
+</usersrepository></source>
+
+ <p>Moreover, per domain base DN can be configured:</p>
+
+ <source>
+ <repository name="LocalUsers" class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository" ldapHost="ldaps://myldapserver:636"
+ principal="uid=ldapUser,ou=system" credentials="password" userBase="ou=People,o=myorg.com,ou=system" userIdAttribute="uid"
+ trustAllCerts="true">
+ <enableVirtualHosting>true</enableVirtualHosting>
+ <domains>
+ <domain.tld>ou=People,o=other.com,ou=system</domain.tld>
+ </domains>
+</usersrepository></source>
</subsection>
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org