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

commit b4b130d0f6816375e2ceb9884d0850b737d8503b
Author: Quan Tran <hqt...@linagora.com>
AuthorDate: Mon Dec 23 16:15:11 2024 +0700

    JAMES-4098 UsersRepository should support multiple administrators
---
 .../partials/configure/usersrepository.adoc        |  6 +++++
 .../sample-configuration/usersrepository.xml       | 12 +++++++++
 .../sample-configuration/usersrepository.xml       |  4 +++
 .../cassandra/CassandraUsersRepositoryTest.java    | 21 ++++++++++++++++
 .../james/user/jpa/JpaUsersRepositoryTest.java     | 21 ++++++++++++++++
 .../apache/james/user/lib/UsersRepositoryImpl.java | 29 ++++++++++++++++++----
 .../james/user/lib/UsersRepositoryContract.java    | 14 +++++++++--
 .../user/memory/MemoryUsersRepositoryTest.java     | 23 +++++++++++++++++
 src/site/xdoc/server/config-users.xml              |  4 +++
 9 files changed, 127 insertions(+), 7 deletions(-)

diff --git a/docs/modules/servers/partials/configure/usersrepository.adoc 
b/docs/modules/servers/partials/configure/usersrepository.adoc
index 390a772528..4bd8d585ad 100644
--- a/docs/modules/servers/partials/configure/usersrepository.adoc
+++ b/docs/modules/servers/partials/configure/usersrepository.adoc
@@ -33,6 +33,12 @@ recipient local part than as login for different protocols.
 |user's name. Allow a user to access to the 
https://tools.ietf.org/html/rfc4616#section-2[impersonation command],
 acting on the behalf of any user.
 
+| administratorIds
+| List of usernames. Allows multiple administrators to access the 
https://tools.ietf.org/html/rfc4616#section-2[impersonation command],
+acting on behalf of any user by specifying multiple `<administratorId>` 
entries inside the `<administratorIds>` block.
+
+Notes: Only one of the above `<administratorId>` property or this 
`<administratorIds>` block should be used to specify the administrator(s).
+
 | verifyFailureDelay
 | Delay after a failed authentication attempt with an invalid user name or 
password. Duration string defaulting to seconds, e.g. `2`, `2s`, `2000ms`. 
Default `0s` (disabled).
 
diff --git 
a/server/apps/distributed-app/sample-configuration/usersrepository.xml 
b/server/apps/distributed-app/sample-configuration/usersrepository.xml
index 4b44b8f7a9..68aaee9587 100644
--- a/server/apps/distributed-app/sample-configuration/usersrepository.xml
+++ b/server/apps/distributed-app/sample-configuration/usersrepository.xml
@@ -24,6 +24,10 @@
     <algorithm>PBKDF2-SHA512</algorithm>
     <enableVirtualHosting>true</enableVirtualHosting>    
     <enableForwarding>true</enableForwarding>
+    <administratorIds>
+        <administratorId>adm...@domain.tld</administratorId>
+        <administratorId>adm...@domain.tld</administratorId>
+    </administratorIds>
 </usersrepository>
 
         <!-- LDAP support example-->
@@ -37,6 +41,10 @@
             userObjectClass="person">
           <enableVirtualHosting>true</enableVirtualHosting>
           <enableForwarding>true</enableForwarding>
+          <administratorIds>
+              <administratorId>adm...@domain.tld</administratorId>
+              <administratorId>adm...@domain.tld</administratorId>
+          </administratorIds>
         </usersrepository>
         -->
 
@@ -51,5 +59,9 @@
             userObjectClass="person">
           <enableVirtualHosting>true</enableVirtualHosting>
           <enableForwarding>true</enableForwarding>
+          <administratorIds>
+              <administratorId>adm...@domain.tld</administratorId>
+              <administratorId>adm...@domain.tld</administratorId>
+          </administratorIds>
         </usersrepository>
         -->
\ No newline at end of file
diff --git a/server/apps/memory-app/sample-configuration/usersrepository.xml 
b/server/apps/memory-app/sample-configuration/usersrepository.xml
index a5390d7140..a0dce2211f 100644
--- a/server/apps/memory-app/sample-configuration/usersrepository.xml
+++ b/server/apps/memory-app/sample-configuration/usersrepository.xml
@@ -24,5 +24,9 @@
     <algorithm>PBKDF2-SHA512</algorithm>
     <enableVirtualHosting>true</enableVirtualHosting>    
     <enableForwarding>true</enableForwarding>
+    <administratorIds>
+        <administratorId>adm...@domain.tld</administratorId>
+        <administratorId>adm...@domain.tld</administratorId>
+    </administratorIds>
 </usersrepository>
 
diff --git 
a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
 
b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
index 3e27065fa3..9def9ef6ae 100644
--- 
a/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
+++ 
b/server/data/data-cassandra/src/test/java/org/apache/james/user/cassandra/CassandraUsersRepositoryTest.java
@@ -20,6 +20,7 @@
 package org.apache.james.user.cassandra;
 
 import java.util.Optional;
+import java.util.Set;
 
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.james.backends.cassandra.CassandraClusterExtension;
@@ -60,6 +61,11 @@ class CassandraUsersRepositoryTest {
         public UsersRepository testee(Optional<Username> administrator) throws 
Exception {
             return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrator);
         }
+
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrators);
+        }
     }
 
     @Nested
@@ -85,6 +91,11 @@ class CassandraUsersRepositoryTest {
         public UsersRepository testee(Optional<Username> administrator) throws 
Exception {
             return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrator);
         }
+
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrators);
+        }
     }
 
     private static UsersRepositoryImpl<CassandraUsersDAO> 
getUsersRepository(DomainList domainList, boolean enableVirtualHosting, 
Optional<Username> administrator) throws Exception {
@@ -97,4 +108,14 @@ class CassandraUsersRepositoryTest {
         usersRepository.configure(configuration);
         return usersRepository;
     }
+
+    private static UsersRepositoryImpl<CassandraUsersDAO> 
getUsersRepository(DomainList domainList, boolean enableVirtualHosting, 
Set<Username> administrators) throws Exception {
+        CassandraUsersDAO usersDAO = new 
CassandraUsersDAO(cassandraCluster.getCassandraCluster().getConf());
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("enableVirtualHosting", 
String.valueOf(enableVirtualHosting));
+        administrators.forEach(admin -> 
configuration.addProperty("administratorIds.administratorId", 
admin.asString()));
+        UsersRepositoryImpl<CassandraUsersDAO> usersRepository = new 
UsersRepositoryImpl<>(domainList, usersDAO);
+        usersRepository.configure(configuration);
+        return usersRepository;
+    }
 }
diff --git 
a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
 
b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
index 55355b0a9d..fc61d90295 100644
--- 
a/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
+++ 
b/server/data/data-jpa/src/test/java/org/apache/james/user/jpa/JpaUsersRepositoryTest.java
@@ -19,6 +19,7 @@
 package org.apache.james.user.jpa;
 
 import java.util.Optional;
+import java.util.Set;
 
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.james.backends.jpa.JpaTestCluster;
@@ -59,6 +60,11 @@ class JpaUsersRepositoryTest {
         public UsersRepository testee(Optional<Username> administrator) throws 
Exception {
             return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrator);
         }
+
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrators);
+        }
     }
 
     @Nested
@@ -84,6 +90,11 @@ class JpaUsersRepositoryTest {
         public UsersRepository testee(Optional<Username> administrator) throws 
Exception {
             return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrator);
         }
+
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            return getUsersRepository(testSystem.getDomainList(), 
extension.isSupportVirtualHosting(), administrators);
+        }
     }
 
     @AfterEach
@@ -100,4 +111,14 @@ class JpaUsersRepositoryTest {
         repos.configure(configuration);
         return repos;
     }
+
+    private static JPAUsersRepository getUsersRepository(DomainList 
domainList, boolean enableVirtualHosting, Set<Username> administrators) throws 
Exception {
+        JPAUsersRepository repos = new JPAUsersRepository(domainList);
+        
repos.setEntityManagerFactory(JPA_TEST_CLUSTER.getEntityManagerFactory());
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("enableVirtualHosting", 
String.valueOf(enableVirtualHosting));
+        administrators.forEach(admin -> 
configuration.addProperty("administratorIds.administratorId", 
admin.asString()));
+        repos.configure(configuration);
+        return repos;
+    }
 }
diff --git 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
index 5007e92555..74bf60bf0d 100644
--- 
a/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
+++ 
b/server/data/data-library/src/main/java/org/apache/james/user/lib/UsersRepositoryImpl.java
@@ -20,8 +20,11 @@
 package org.apache.james.user.lib;
 
 import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
 import java.util.Iterator;
+import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 
 import jakarta.inject.Inject;
 
@@ -47,6 +50,7 @@ import org.slf4j.LoggerFactory;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.base.CharMatcher;
+import com.google.common.collect.ImmutableSet;
 
 import reactor.core.publisher.Mono;
 
@@ -57,7 +61,7 @@ public class UsersRepositoryImpl<T extends UsersDAO> 
implements UsersRepository,
     private final DomainList domainList;
     protected final T usersDAO;
     private boolean virtualHosting;
-    private Optional<Username> administratorId;
+    private Set<Username> administratorIds;
     private long verifyFailureDelay;
     private UserEntityValidator validator;
 
@@ -76,13 +80,29 @@ public class UsersRepositoryImpl<T extends UsersDAO> 
implements UsersRepository,
     @Override
     public void configure(HierarchicalConfiguration<ImmutableNode> 
configuration) throws ConfigurationException {
         virtualHosting = configuration.getBoolean("enableVirtualHosting", 
usersDAO.getDefaultVirtualHostingValue());
-        administratorId = 
Optional.ofNullable(configuration.getString("administratorId"))
-            .map(Username::of);
+
+        administratorIds = parseAdministratorId(configuration)
+            .orElseGet(() -> parseAdministratorIds(configuration));
+
         verifyFailureDelay = 
Optional.ofNullable(configuration.getString("verifyFailureDelay"))
             .map(string -> DurationParser.parse(string, 
ChronoUnit.SECONDS).toMillis())
             .orElse(0L);
     }
 
+    private Optional<Set<Username>> 
parseAdministratorId(HierarchicalConfiguration<ImmutableNode> configuration) {
+        return Optional.ofNullable(configuration.getString("administratorId"))
+            .map(id -> ImmutableSet.of(Username.of(id)));
+    }
+
+    private Set<Username> 
parseAdministratorIds(HierarchicalConfiguration<ImmutableNode> configuration) {
+        return configuration.configurationsAt("administratorIds")
+            .stream()
+            .flatMap(e -> Arrays.stream(e.getStringArray("administratorId")))
+            .filter(Objects::nonNull)
+            .map(Username::of)
+            .collect(ImmutableSet.toImmutableSet());
+    }
+
     public void setEnableVirtualHosting(boolean virtualHosting) {
         this.virtualHosting = virtualHosting;
     }
@@ -249,8 +269,7 @@ public class UsersRepositoryImpl<T extends UsersDAO> 
implements UsersRepository,
     public boolean isAdministrator(Username username) throws 
UsersRepositoryException {
         assertValid(username);
 
-        return administratorId.map(id -> id.equals(username))
-            .orElse(false);
+        return administratorIds.contains(username);
     }
 
     @Override
diff --git 
a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
 
b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
index 44ce8d6a83..9f2816baa5 100644
--- 
a/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
+++ 
b/server/data/data-library/src/test/java/org/apache/james/user/lib/UsersRepositoryContract.java
@@ -26,6 +26,7 @@ import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Stream;
 
 import org.apache.james.core.Domain;
@@ -149,6 +150,7 @@ public interface UsersRepositoryContract {
 
     UsersRepository testee(Optional<Username> administrator) throws Exception;
 
+    UsersRepository testee(Set<Username> administrators) throws Exception;
 
     interface ReadOnlyContract extends UsersRepositoryContract {
         @Test
@@ -528,11 +530,19 @@ public interface UsersRepositoryContract {
             assertThat(testee.isAdministrator(testSystem.admin)).isTrue();
         }
 
+        @Test
+        default void 
isAdministratorShouldReturnTrueWhenConfiguredMultipleAdminsAndUserIsAdmin(TestSystem
 testSystem) throws Exception {
+            UsersRepository testee = testee(Set.of(testSystem.admin, 
testSystem.user1));
+
+            assertThat(testee.isAdministrator(testSystem.admin)).isTrue();
+            assertThat(testee.isAdministrator(testSystem.user1)).isTrue();
+        }
+
         @Test
         default void 
isAdministratorShouldReturnFalseWhenConfiguredAndUserIsNotAdmin(TestSystem 
testSystem) throws Exception {
-            UsersRepository testee = testee(Optional.of(testSystem.admin));
+            UsersRepository testee = testee(Set.of(testSystem.admin, 
testSystem.user1));
 
-            assertThat(testee.isAdministrator(testSystem.user1)).isFalse();
+            assertThat(testee.isAdministrator(testSystem.user2)).isFalse();
         }
     }
 
diff --git 
a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
 
b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
index c37356d1fd..2d8c892846 100644
--- 
a/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
+++ 
b/server/data/data-memory/src/test/java/org/apache/james/user/memory/MemoryUsersRepositoryTest.java
@@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.Optional;
+import java.util.Set;
 
 import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.commons.configuration2.HierarchicalConfiguration;
@@ -71,6 +72,13 @@ class MemoryUsersRepositoryTest {
             return memoryUsersRepository;
         }
 
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            MemoryUsersRepository memoryUsersRepository = 
MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+            memoryUsersRepository.configure(configuration(administrators, 
true));
+            return memoryUsersRepository;
+        }
+
         @Test
         void assertValidShouldThrowWhenNoDomainPartAndVirtualHosting() {
             assertThatThrownBy(() -> 
memoryUsersRepository.assertValid(Username.of("user")))
@@ -137,6 +145,13 @@ class MemoryUsersRepositoryTest {
             return memoryUsersRepository;
         }
 
+        @Override
+        public UsersRepository testee(Set<Username> administrators) throws 
Exception {
+            MemoryUsersRepository memoryUsersRepository = 
MemoryUsersRepository.withVirtualHosting(testSystem.getDomainList());
+            memoryUsersRepository.configure(configuration(administrators, 
false));
+            return memoryUsersRepository;
+        }
+
         @Test
         void 
assertValidShouldThrowWhenDomainPartAndNoVirtualHosting(TestSystem testSystem) {
             MemoryUsersRepository memoryUsersRepository = 
MemoryUsersRepository.withoutVirtualHosting(testSystem.getDomainList());
@@ -162,4 +177,12 @@ class MemoryUsersRepositoryTest {
         configuration.addProperty("algorithm", "none");
         return configuration;
     }
+
+    private HierarchicalConfiguration<ImmutableNode> 
configuration(Set<Username> administrators, boolean enableVirtualHosting) {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        administrators.forEach(admin -> 
configuration.addProperty("administratorIds.administratorId", 
admin.asString()));
+        configuration.addProperty("enableVirtualHosting", 
enableVirtualHosting);
+        configuration.addProperty("algorithm", "none");
+        return configuration;
+    }
 }
diff --git a/src/site/xdoc/server/config-users.xml 
b/src/site/xdoc/server/config-users.xml
index 0efe4cb5b9..5ea75be4e9 100644
--- a/src/site/xdoc/server/config-users.xml
+++ b/src/site/xdoc/server/config-users.xml
@@ -61,6 +61,10 @@
         <dd>true or false. Add domain support for users (default: false, 
except for Cassandra Users Repository)</dd>
         <dt><strong>administratorId</strong></dt>
         <dd>user's name. Allow a user to access to the <a 
href="https://tools.ietf.org/html/rfc4616#section-2";>impersonation command</a>, 
acting on the behalf of any user.</dd>
+        <dt><strong>administratorIds</strong></dt>
+        <dd>List of usernames. Allows multiple administrators to access the <a 
href="https://tools.ietf.org/html/rfc4616#section-2";>impersonation command</a>, 
acting on behalf of any user by specifying multiple
+              <code>&lt;administratorId&gt;</code> entries inside the 
<code>&lt;administratorIds&gt;</code> block.</dd>
+        <dd><em>Note:</em> Only one of the above 
<code>&lt;administratorId&gt;</code> property or this 
<code>&lt;administratorIds&gt;</code> block should be used to specify the 
administrator(s).</dd>
         <dt><strong>verifyFailureDelay</strong></dt>
         <dd>2, 2s, 2000ms, default 0s (disabled). Delay after a failed 
authentication attempt with an invalid user name or password.</dd>
       </dl>


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

Reply via email to