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]

Reply via email to