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 <btell...@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("b...@nonexistant.org"))).isFalse(); + } + + @Test + void shouldContainEntriesInExtraDN() throws Exception { + assertThat(usersLDAPRepository.contains(Username.of("b...@extra.org"))).isTrue(); + + assertThat(usersLDAPRepository.countUsers()).isEqualTo(2); + + assertThat(ImmutableList.copyOf(usersLDAPRepository.list())) + .containsOnly(JAMES_USER_MAIL, Username.of("b...@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("b...@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-u...@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: b...@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-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org