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>
-&lt;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"/&gt;</source>
+&lt;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"&gt;
+           &lt;enableVirtualHosting&gt;true&lt;/enableVirtualHosting&gt;
+&lt;/usersrepository&gt;
+       </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>
  &lt;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"/&gt;</source>
+     trustAllCerts="true"&gt;
+            &lt;enableVirtualHosting&gt;true&lt;/enableVirtualHosting&gt;
+&lt;/usersrepository&gt;</source>
+
+        <p>Moreover, per domain base DN can be configured:</p>
+
+        <source>
+            &lt;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"&gt;
+            &lt;enableVirtualHosting&gt;true&lt;/enableVirtualHosting&gt;
+            &lt;domains&gt;
+                &lt;domain.tld&gt;ou=People,o=other.com,ou=system&lt;/domain.tld&gt;
+            &lt;/domains&gt;
+&lt;/usersrepository&gt;</source>
 
      </subsection>
 


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org