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 98a3309159a75124ff1445bbd54ae0ad6501a904 Author: Benoit TELLIER <[email protected]> AuthorDate: Mon Jan 5 17:12:46 2026 +0100 JAMES-4158 Allow IMAP to specify per-port administrators This allows to effectively expose admin users onto the private network. --- .../apache/james/imap/api/ImapConfiguration.java | 56 +++++++++++++++------- .../imap/processor/AbstractAuthProcessor.java | 11 +++++ .../imap/processor/AuthenticateProcessor.java | 1 + .../apache/james/imapserver/netty/IMAPServer.java | 25 +++++----- .../james/imapserver/netty/IMAPServerTest.java | 36 ++++++++++++++ .../src/test/resources/imapServerAdminUsers.xml | 21 ++++++++ 6 files changed, 121 insertions(+), 29 deletions(-) diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java b/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java index 7aa919c10d..df0fedc0d4 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/api/ImapConfiguration.java @@ -20,6 +20,7 @@ package org.apache.james.imap.api; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; @@ -31,6 +32,7 @@ import org.apache.james.imap.api.message.Capability; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -65,6 +67,7 @@ public class ImapConfiguration { private Optional<Boolean> provisionDefaultMailboxes; private Optional<Properties> customProperties; private ImmutableSet<String> additionalConnectionChecks; + private ImmutableList<String> adminUsers; private ImmutableMap<String, String> idFieldsResponse; private Builder() { @@ -80,6 +83,7 @@ public class ImapConfiguration { this.customProperties = Optional.empty(); this.additionalConnectionChecks = ImmutableSet.of(); this.idFieldsResponse = ImmutableMap.of(); + this.adminUsers = ImmutableList.of(); } public Builder idleTimeInterval(long idleTimeInterval) { @@ -114,6 +118,11 @@ public class ImapConfiguration { return this; } + public Builder adminUsers(ImmutableList<String> adminUsers) { + this.adminUsers = adminUsers; + return this; + } + public Builder disabledCaps(String... disableCaps) { this.disabledCaps = ImmutableSet.copyOf(disableCaps); return this; @@ -166,23 +175,25 @@ public class ImapConfiguration { public ImapConfiguration build() { ImmutableSet<Capability> normalizeDisableCaps = disabledCaps.stream() - .filter(Builder::noBlankString) - .map(StringUtils::normalizeSpace) - .map(Capability::of) - .collect(ImmutableSet.toImmutableSet()); + .filter(Builder::noBlankString) + .map(StringUtils::normalizeSpace) + .map(Capability::of) + .collect(ImmutableSet.toImmutableSet()); + return new ImapConfiguration( - appendLimit, - enableIdle.orElse(DEFAULT_ENABLE_IDLE), - idleTimeInterval.orElse(DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS), - concurrentRequests.orElse(DEFAULT_CONCURRENT_REQUESTS), - maxQueueSize.orElse(DEFAULT_QUEUE_SIZE), - idleTimeIntervalUnit.orElse(DEFAULT_HEARTBEAT_INTERVAL_UNIT), - normalizeDisableCaps, - isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE), - provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES), - customProperties.orElseGet(Properties::new), - additionalConnectionChecks, - idFieldsResponse); + appendLimit, + enableIdle.orElse(DEFAULT_ENABLE_IDLE), + idleTimeInterval.orElse(DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS), + concurrentRequests.orElse(DEFAULT_CONCURRENT_REQUESTS), + maxQueueSize.orElse(DEFAULT_QUEUE_SIZE), + idleTimeIntervalUnit.orElse(DEFAULT_HEARTBEAT_INTERVAL_UNIT), + normalizeDisableCaps, + isCondstoreEnable.orElse(DEFAULT_CONDSTORE_DISABLE), + provisionDefaultMailboxes.orElse(DEFAULT_PROVISION_DEFAULT_MAILBOXES), + customProperties.orElseGet(Properties::new), + additionalConnectionChecks, + adminUsers, + idFieldsResponse); } } @@ -197,6 +208,7 @@ public class ImapConfiguration { private final boolean provisionDefaultMailboxes; private final Properties customProperties; private final ImmutableSet<String> additionalConnectionChecks; + private final List<String> adminUsers; private final ImmutableMap<String, String> idFieldsResponse; private ImapConfiguration(Optional<Long> appendLimit, @@ -210,6 +222,7 @@ public class ImapConfiguration { boolean provisionDefaultMailboxes, Properties customProperties, ImmutableSet<String> additionalConnectionChecks, + List<String> adminUsers, ImmutableMap<String, String> idFieldsResponse) { this.appendLimit = appendLimit; this.enableIdle = enableIdle; @@ -222,6 +235,7 @@ public class ImapConfiguration { this.provisionDefaultMailboxes = provisionDefaultMailboxes; this.customProperties = customProperties; this.additionalConnectionChecks = additionalConnectionChecks; + this.adminUsers = adminUsers; this.idFieldsResponse = idFieldsResponse; } @@ -277,6 +291,10 @@ public class ImapConfiguration { return idFieldsResponse; } + public List<String> getAdminUsers() { + return adminUsers; + } + @Override public final boolean equals(Object obj) { if (obj instanceof ImapConfiguration that) { @@ -291,7 +309,8 @@ public class ImapConfiguration { && Objects.equal(that.getCustomProperties(), customProperties) && Objects.equal(that.isCondstoreEnable(), isCondstoreEnable) && Objects.equal(that.getAdditionalConnectionChecks(), additionalConnectionChecks) - && Objects.equal(that.getIdFieldsResponse(), idFieldsResponse); + && Objects.equal(that.getIdFieldsResponse(), idFieldsResponse) + && Objects.equal(that.getAdminUsers(), adminUsers); } return false; } @@ -300,7 +319,7 @@ public class ImapConfiguration { public final int hashCode() { return Objects.hashCode(enableIdle, idleTimeInterval, idleTimeIntervalUnit, disabledCaps, isCondstoreEnable, concurrentRequests, maxQueueSize, appendLimit, provisionDefaultMailboxes, customProperties, additionalConnectionChecks, - idFieldsResponse); + idFieldsResponse, adminUsers); } @Override @@ -318,6 +337,7 @@ public class ImapConfiguration { .add("customProperties", customProperties) .add("additionalConnectionChecks", additionalConnectionChecks) .add("idFieldsResponse", idFieldsResponse) + .add("adminUsers", adminUsers) .toString(); } } diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java index d37017eaf3..34cc3eb3be 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java @@ -27,6 +27,7 @@ import org.apache.james.imap.api.message.request.ImapRequest; import org.apache.james.imap.api.message.response.StatusResponseFactory; import org.apache.james.imap.api.process.ImapSession; import org.apache.james.imap.main.PathConverter; +import org.apache.james.mailbox.Authorizator; import org.apache.james.mailbox.DefaultMailboxes; import org.apache.james.mailbox.MailboxManager; import org.apache.james.mailbox.MailboxSession; @@ -126,6 +127,7 @@ public abstract class AbstractAuthProcessor<R extends ImapRequest> extends Abstr } Username otherUser = authenticationAttempt.getDelegateUserName().orElseThrow(); doAuthWithDelegation(() -> getMailboxManager() + .withExtraAuthorizator(withAdminUsers()) .authenticate(givenUser, authenticationAttempt.getPassword()) .as(otherUser), session, @@ -133,6 +135,15 @@ public abstract class AbstractAuthProcessor<R extends ImapRequest> extends Abstr givenUser, otherUser); } + protected Authorizator withAdminUsers() { + return (userId, otherUserId) -> { + if (imapConfiguration.getAdminUsers().contains(userId.asString())) { + return Authorizator.AuthorizationState.ALLOWED; + } + return Authorizator.AuthorizationState.FORBIDDEN; + }; + } + protected void doAuthWithDelegation(MailboxSessionAuthWithDelegationSupplier mailboxSessionSupplier, ImapSession session, ImapRequest request, Responder responder, Username authenticateUser, Username delegatorUser) { diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java index 7db61a86f0..cd22c4ca68 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java @@ -215,6 +215,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser()); if (!associatedUser.equals(authenticatedUser)) { doAuthWithDelegation(() -> getMailboxManager() + .withExtraAuthorizator(withAdminUsers()) .authenticate(authenticatedUser) .as(associatedUser), session, request, responder, authenticatedUser, associatedUser); diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java index e0295b4e09..89a2469a07 100644 --- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java +++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java @@ -67,6 +67,7 @@ import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -249,19 +250,21 @@ public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapC @VisibleForTesting static ImapConfiguration getImapConfiguration(HierarchicalConfiguration<ImmutableNode> configuration) { ImmutableSet<String> disabledCaps = ImmutableSet.copyOf(Splitter.on(CAPABILITY_SEPARATOR).split(configuration.getString("disabledCaps", ""))); + ImmutableList<String> adminUsers = ImmutableList.copyOf(configuration.getStringArray("auth.adminUsers.adminUser")); return ImapConfiguration.builder() - .enableIdle(configuration.getBoolean("enableIdle", ImapConfiguration.DEFAULT_ENABLE_IDLE)) - .idleTimeInterval(configuration.getLong("idleTimeInterval", ImapConfiguration.DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS)) - .idleTimeIntervalUnit(getTimeIntervalUnit(configuration.getString("idleTimeIntervalUnit", DEFAULT_TIME_UNIT))) - .disabledCaps(disabledCaps) - .appendLimit(Optional.of(parseLiteralSizeLimit(configuration)).filter(i -> i > 0)) - .maxQueueSize(configuration.getInteger("maxQueueSize", ImapConfiguration.DEFAULT_QUEUE_SIZE)) - .concurrentRequests(configuration.getInteger("concurrentRequests", ImapConfiguration.DEFAULT_CONCURRENT_REQUESTS)) - .isProvisionDefaultMailboxes(configuration.getBoolean("provisionDefaultMailboxes", ImapConfiguration.DEFAULT_PROVISION_DEFAULT_MAILBOXES)) - .withCustomProperties(configuration.getProperties("customProperties")) - .idFieldsResponse(getIdCommandResponseFields(configuration)) - .build(); + .enableIdle(configuration.getBoolean("enableIdle", ImapConfiguration.DEFAULT_ENABLE_IDLE)) + .idleTimeInterval(configuration.getLong("idleTimeInterval", ImapConfiguration.DEFAULT_HEARTBEAT_INTERVAL_IN_SECONDS)) + .idleTimeIntervalUnit(getTimeIntervalUnit(configuration.getString("idleTimeIntervalUnit", DEFAULT_TIME_UNIT))) + .disabledCaps(disabledCaps) + .appendLimit(Optional.of(parseLiteralSizeLimit(configuration)).filter(i -> i > 0)) + .maxQueueSize(configuration.getInteger("maxQueueSize", ImapConfiguration.DEFAULT_QUEUE_SIZE)) + .concurrentRequests(configuration.getInteger("concurrentRequests", ImapConfiguration.DEFAULT_CONCURRENT_REQUESTS)) + .isProvisionDefaultMailboxes(configuration.getBoolean("provisionDefaultMailboxes", ImapConfiguration.DEFAULT_PROVISION_DEFAULT_MAILBOXES)) + .withCustomProperties(configuration.getProperties("customProperties")) + .idFieldsResponse(getIdCommandResponseFields(configuration)) + .adminUsers(adminUsers) + .build(); } private static ImmutableMap<String, String> getIdCommandResponseFields(HierarchicalConfiguration<ImmutableNode> configuration) { diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java index c123de34ed..e46c354152 100644 --- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java +++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java @@ -45,6 +45,7 @@ import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.security.cert.X509Certificate; import java.time.Duration; +import java.util.Base64; import java.util.List; import java.util.Properties; import java.util.Set; @@ -1063,6 +1064,41 @@ class IMAPServerTest { } } + @Nested + class AdminUsers { + IMAPServer imapServer; + private int port; + private SocketChannel clientConnection; + + @BeforeEach + void beforeEach() throws Exception { + imapServer = createImapServer("imapServerAdminUsers.xml"); + port = imapServer.getListenAddresses().get(0).getPort(); + + + clientConnection = SocketChannel.open(); + clientConnection.connect(new InetSocketAddress(LOCALHOST_IP, port)); + readBytes(clientConnection); + } + + @AfterEach + void tearDown() throws Exception { + clientConnection.close(); + imapServer.destroy(); + } + + @Test + void shouldSupportPerPortAdminUsers() throws Exception { + clientConnection.write(ByteBuffer.wrap("a0 AUTHENTICATE PLAIN\r\n".getBytes(StandardCharsets.UTF_8))); + readStringUntil(clientConnection, s -> s.startsWith("+")); + clientConnection.write(ByteBuffer.wrap((Base64.getEncoder().encodeToString((USER2.asString() + "\0" + USER.asString() + "\0" + USER_PASS).getBytes(StandardCharsets.US_ASCII)) + "\r\n").getBytes(StandardCharsets.US_ASCII))); + + String reply = readStringUntil(clientConnection, s -> s.startsWith("a0")).getLast(); + + assertThat(reply).startsWith("a0 OK"); + } + } + @Nested class PlainAuthDisabled { IMAPServer imapServer; diff --git a/server/protocols/protocols-imap4/src/test/resources/imapServerAdminUsers.xml b/server/protocols/protocols-imap4/src/test/resources/imapServerAdminUsers.xml new file mode 100644 index 0000000000..fbe4fcab2d --- /dev/null +++ b/server/protocols/protocols-imap4/src/test/resources/imapServerAdminUsers.xml @@ -0,0 +1,21 @@ +<imapserver enabled="true"> + <jmxName>imapserver</jmxName> + <bind>0.0.0.0:0</bind> + <connectionBacklog>200</connectionBacklog> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <idleTimeInterval>120</idleTimeInterval> + <idleTimeIntervalUnit>SECONDS</idleTimeIntervalUnit> + <enableIdle>true</enableIdle> + <inMemorySizeLimit>64K</inMemorySizeLimit> + <literalSizeLimit>128K</literalSizeLimit> + <plainAuthDisallowed>false</plainAuthDisallowed> + <gracefulShutdown>false</gracefulShutdown> + <concurrentRequests>20</concurrentRequests> + <auth> + <adminUsers> + <adminUser>[email protected]</adminUser> + <adminUser>[email protected]</adminUser> + </adminUsers> + </auth> +</imapserver> \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
