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><administratorId></code> entries inside the <code><administratorIds></code> block.</dd> + <dd><em>Note:</em> Only one of the above <code><administratorId></code> property or this <code><administratorIds></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