This is an automated email from the ASF dual-hosted git repository.

quantranhong1999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 20a1ea25c4af5505f225e6989d5b4d97a6289a18
Author: Quan Tran <[email protected]>
AuthorDate: Thu Aug 13 09:43:55 2026 +0700

    JAMES-4210 Resolve ManageSieve SASL mechanisms per server configuration
    
    Construct each ManageSieve processor from its server authentication 
configuration. Support Spring built-in SASL factories, preserve the legacy root 
OIDC configuration as an alias, and reject ambiguous duplicate OIDC 
configuration.
---
 server/protocols/protocols-managesieve/pom.xml     |  12 +-
 .../netty/ManageSieveChannelUpstreamHandler.java   |   9 +-
 .../managesieveserver/netty/ManageSieveServer.java |  17 +--
 .../netty/ManageSieveServerFactory.java            | 140 ++++++++++++++++++---
 .../META-INF/spring/managesieveserver-context.xml  |   6 +-
 .../ManageSieveServerTestSystem.java               |  43 +++++--
 .../netty/ManageSieveServerFactoryTest.java        | 138 ++++++++++++++++++++
 .../src/test/resources/managesieveserver-oidc.xml  |  22 ++--
 8 files changed, 320 insertions(+), 67 deletions(-)

diff --git a/server/protocols/protocols-managesieve/pom.xml 
b/server/protocols/protocols-managesieve/pom.xml
index 0c7f1e5b1d..a60fc92a8e 100644
--- a/server/protocols/protocols-managesieve/pom.xml
+++ b/server/protocols/protocols-managesieve/pom.xml
@@ -13,6 +13,10 @@
     <name>Apache James :: Server :: ManageSieve</name>
 
     <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>apache-james-mailbox-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-file</artifactId>
@@ -76,6 +80,10 @@
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-netty</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-sasl</artifactId>
+        </dependency>
         <dependency>
             <groupId>commons-net</groupId>
             <artifactId>commons-net</artifactId>
@@ -85,10 +93,6 @@
             <groupId>io.netty</groupId>
             <artifactId>netty-handler</artifactId>
         </dependency>
-        <dependency>
-            <groupId>jakarta.annotation</groupId>
-            <artifactId>jakarta.annotation-api</artifactId>
-        </dependency>
         <dependency>
             <groupId>jakarta.inject</groupId>
             <artifactId>jakarta.inject-api</artifactId>
diff --git 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
index 13e53203d9..4b89a59aac 100644
--- 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
+++ 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
@@ -21,9 +21,7 @@ package org.apache.james.managesieveserver.netty;
 
 import java.io.Closeable;
 import java.net.InetSocketAddress;
-import java.util.Optional;
 
-import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.SessionTerminatedException;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
@@ -53,14 +51,11 @@ public class ManageSieveChannelUpstreamHandler extends 
ChannelInboundHandlerAdap
     private final ManageSieveProcessor manageSieveProcessor;
     private final Encryption secure;
     private final int maxLineLength;
-    private final Optional<OidcSASLConfiguration> oidcConfiguration;
 
-    public ManageSieveChannelUpstreamHandler(
-        ManageSieveProcessor manageSieveProcessor, Encryption secure, int 
maxLineLength, Optional<OidcSASLConfiguration> oidcConfiguration) {
+    public ManageSieveChannelUpstreamHandler(ManageSieveProcessor 
manageSieveProcessor, Encryption secure, int maxLineLength) {
         this.manageSieveProcessor = manageSieveProcessor;
         this.secure = secure;
         this.maxLineLength = maxLineLength;
-        this.oidcConfiguration = oidcConfiguration;
     }
 
     private boolean isSSL() {
@@ -160,7 +155,7 @@ public class ManageSieveChannelUpstreamHandler extends 
ChannelInboundHandlerAdap
             LOGGER.info("Connection established from {}", 
address.getAddress().getHostAddress());
 
             Session session = new SettableSession();
-            session.setOidcSASLConfiguration(this.oidcConfiguration);
+            session.setStartTlsSupported(supportsStartTLS());
             if (isSSL()) {
                 session.setSslEnabled(true);
             }
diff --git 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
index 7e1f55af7c..bd2e8abaf6 100644
--- 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
+++ 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
@@ -19,14 +19,11 @@
 
 package org.apache.james.managesieveserver.netty;
 
-import java.net.MalformedURLException;
-import java.net.URISyntaxException;
 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.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.netty.AbstractChannelPipelineFactory;
@@ -51,13 +48,10 @@ import io.netty.util.CharsetUtil;
 public class ManageSieveServer extends AbstractConfigurableAsyncServer 
implements ManageSieveServerMBean {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ManageSieveServer.class);
-    static final String OIDC_PATH = "oidc";
-
     private final int maxLineLength;
     private final ManageSieveProcessor manageSieveProcessor;
     private Optional<ConnectionLimitUpstreamHandler> 
connectionLimitUpstreamHandler = Optional.empty();
     private Optional<ConnectionPerIpLimitUpstreamHandler> 
connectionPerIpLimitUpstreamHandler = Optional.empty();
-    private Optional<OidcSASLConfiguration> oidcConfiguration;
 
     public ManageSieveServer(int maxLineLength, ManageSieveProcessor 
manageSieveProcessor) {
         this.maxLineLength = maxLineLength;
@@ -76,15 +70,6 @@ public class ManageSieveServer extends 
AbstractConfigurableAsyncServer implement
         connectionLimitUpstreamHandler = 
ConnectionLimitUpstreamHandler.forCount(connectionLimit);
         connectionPerIpLimitUpstreamHandler = 
ConnectionPerIpLimitUpstreamHandler.forCount(connPerIP);
 
-        if (config.immutableChildConfigurationsAt(OIDC_PATH).isEmpty()) {
-            this.oidcConfiguration = Optional.empty();
-        } else {
-            try {
-                this.oidcConfiguration = 
Optional.of(OidcSASLConfiguration.parse(config.configurationAt(OIDC_PATH)));
-            } catch (MalformedURLException | NullPointerException | 
URISyntaxException exception) {
-                throw new ConfigurationException("Failed to parse OIDC 
configuration", exception);
-            }
-        }
     }
 
     @Override
@@ -94,7 +79,7 @@ public class ManageSieveServer extends 
AbstractConfigurableAsyncServer implement
 
     @Override
     protected ChannelInboundHandlerAdapter createCoreHandler() {
-        return new ManageSieveChannelUpstreamHandler(manageSieveProcessor, 
getEncryption(), maxLineLength, this.oidcConfiguration);
+        return new ManageSieveChannelUpstreamHandler(manageSieveProcessor, 
getEncryption(), maxLineLength);
     }
 
     @Override
diff --git 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactory.java
 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactory.java
index b345dcaf86..2700954c7b 100644
--- 
a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactory.java
+++ 
b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactory.java
@@ -20,32 +20,112 @@
 package org.apache.james.managesieveserver.netty;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 
-import jakarta.annotation.PostConstruct;
 import jakarta.inject.Inject;
 
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
 import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.mailbox.Authenticator;
+import org.apache.james.mailbox.Authorizator;
 import org.apache.james.managesieve.core.CoreProcessor;
 import org.apache.james.managesieve.jsieve.Parser;
 import org.apache.james.managesieve.transcode.ArgumentParser;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismFactory;
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.lib.netty.AbstractServerFactory;
 import org.apache.james.protocols.netty.Encryption;
+import org.apache.james.protocols.sasl.BuiltInSaslMechanismFactories;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
+import org.apache.james.protocols.sasl.OauthBearerSaslMechanismFactory;
+import org.apache.james.protocols.sasl.PlainSaslMechanismFactory;
+import org.apache.james.protocols.sasl.XOauth2SaslMechanismFactory;
 import org.apache.james.sieverepository.api.SieveRepository;
-import org.apache.james.user.api.UsersRepository;
+
+import com.github.fge.lambdas.Throwing;
+import com.google.common.collect.ImmutableList;
 
 public class ManageSieveServerFactory extends AbstractServerFactory {
+    @FunctionalInterface
+    public interface ManageSieveSaslMechanismLoader {
+        static ManageSieveSaslMechanismLoader defaultLoader() {
+            ImmutableList<SaslMechanismFactory> defaultFactories = 
ImmutableList.of(
+                new PlainSaslMechanismFactory(false),
+                new OauthBearerSaslMechanismFactory(),
+                new XOauth2SaslMechanismFactory());
+            return configuration -> loadBuiltInMechanisms(defaultFactories, 
normalizeSaslConfiguration(configuration));
+        }
+
+        ImmutableList<SaslMechanism> 
load(HierarchicalConfiguration<ImmutableNode> configuration) throws 
ConfigurationException;
+    }
+
+    private static final Authorizator DENY_DELEGATION = (authenticationId, 
authorizationId) -> Authorizator.AuthorizationState.FORBIDDEN;
+
+    private static ImmutableList<SaslMechanism> 
loadBuiltInMechanisms(ImmutableList<SaslMechanismFactory> defaultFactories,
+                                                                      
HierarchicalConfiguration<ImmutableNode> configuration) throws 
ConfigurationException {
+        ImmutableList<SaslMechanismFactory> enabledFactories = 
BuiltInSaslMechanismFactories.enabledForServer(defaultFactories, configuration);
+        ImmutableList<String> configuredFactories = 
retrieveSaslMechanismFactoryClassNames(configuration);
+        ImmutableList<SaslMechanismFactory> selectedFactories = 
configuredFactories.isEmpty()
+            ? enabledFactories
+            : configuredFactories.stream()
+                .map(className -> findBuiltInFactory(className, 
enabledFactories))
+                .collect(ImmutableList.toImmutableList());
+        try {
+            return selectedFactories.stream()
+                .map(Throwing.function(factory -> 
factory.create(configuration)))
+                .collect(ImmutableList.toImmutableList());
+        } catch (RuntimeException e) {
+            if (e.getCause() instanceof ConfigurationException 
configurationException) {
+                throw configurationException;
+            }
+            throw e;
+        }
+    }
+
+    private static SaslMechanismFactory findBuiltInFactory(String className,
+                                                           
ImmutableList<SaslMechanismFactory> enabledFactories) {
+        return enabledFactories.stream()
+            .filter(factory -> 
className.equals(factory.getClass().getCanonicalName())
+                || (!className.contains(".") && 
className.equals(factory.getClass().getSimpleName())))
+            .findFirst()
+            .orElseThrow(() -> new IllegalArgumentException("Unsupported SASL 
mechanism factory: " + className));
+    }
+
+    public static ImmutableList<String> 
retrieveSaslMechanismFactoryClassNames(HierarchicalConfiguration<ImmutableNode> 
configuration) throws ConfigurationException {
+        if (!configuration.containsKey("auth.saslMechanisms")) {
+            return ImmutableList.of();
+        }
+        ImmutableList<String> factoryClassNames = 
Arrays.stream(configuration.getStringArray("auth.saslMechanisms"))
+            .flatMap(value -> Arrays.stream(value.split(",")))
+            .map(String::trim)
+            .collect(ImmutableList.toImmutableList());
+        if (factoryClassNames.isEmpty() || 
factoryClassNames.stream().anyMatch(String::isBlank)) {
+            throw new ConfigurationException("auth.saslMechanisms must not be 
blank when configured");
+        }
+        return factoryClassNames;
+    }
 
     private FileSystem fileSystem;
     private Encryption.Factory encryptionFactory;
-    private ManageSieveProcessor manageSieveProcessor;
     private SieveRepository sieveRepository;
-    private UsersRepository usersRepository;
+    private Authenticator authenticator;
     private Parser sieveParser;
+    private ManageSieveSaslMechanismLoader saslMechanismLoader = 
ManageSieveSaslMechanismLoader.defaultLoader();
+
+    public ManageSieveServerFactory() {
+    }
+
+    @Inject
+    public ManageSieveServerFactory(ManageSieveSaslMechanismLoader 
saslMechanismLoader) {
+        this.saslMechanismLoader = saslMechanismLoader;
+    }
 
     @Inject
     public void setFileSystem(FileSystem fileSystem) {
@@ -63,8 +143,8 @@ public class ManageSieveServerFactory extends 
AbstractServerFactory {
     }
 
     @Inject
-    public void setUsersRepository(UsersRepository usersRepository) {
-        this.usersRepository = usersRepository;
+    public void setAuthenticator(Authenticator authenticator) {
+        this.authenticator = authenticator;
     }
 
     @Inject
@@ -72,27 +152,53 @@ public class ManageSieveServerFactory extends 
AbstractServerFactory {
         this.sieveParser = sieveParser;
     }
 
-
-    @Override
-    @PostConstruct
-    public void init() throws Exception {
-        manageSieveProcessor = new ManageSieveProcessor(new ArgumentParser(new 
CoreProcessor(sieveRepository, usersRepository, sieveParser)));
-        super.init();
-    }
-
     @Override
     protected List<AbstractConfigurableAsyncServer> 
createServers(HierarchicalConfiguration<ImmutableNode> config) throws Exception 
{
         List<AbstractConfigurableAsyncServer> servers = new ArrayList<>();
         List<HierarchicalConfiguration<ImmutableNode>> configs = 
config.configurationsAt("managesieveserver");
 
-        for (HierarchicalConfiguration<ImmutableNode> serverConfig: configs) {
-            ManageSieveServer server = new ManageSieveServer(8000, 
manageSieveProcessor);
+        for (HierarchicalConfiguration<ImmutableNode> serverConfig : configs) {
+            HierarchicalConfiguration<ImmutableNode> saslConfiguration = 
normalizeSaslConfiguration(serverConfig);
+            ImmutableList<SaslMechanism> saslMechanisms = 
saslMechanismLoader.load(saslConfiguration);
+            ManageSieveProcessor processor = new ManageSieveProcessor(
+                new ArgumentParser(new CoreProcessor(sieveRepository, 
sieveParser, saslMechanisms)),
+                saslMechanisms,
+                new JamesSaslAuthenticator(authenticator, DENY_DELEGATION));
+            ManageSieveServer server = new ManageSieveServer(8000, processor);
             server.setFileSystem(fileSystem);
             server.setEncryptionFactory(encryptionFactory);
             server.configure(serverConfig);
             servers.add(server);
         }
-
         return servers;
     }
+
+    static HierarchicalConfiguration<ImmutableNode> 
normalizeSaslConfiguration(HierarchicalConfiguration<ImmutableNode> 
serverConfiguration) throws ConfigurationException {
+        if 
(!serverConfiguration.immutableConfigurationsAt("auth.oidc").isEmpty()) {
+            if 
(!serverConfiguration.immutableConfigurationsAt("oidc").isEmpty()) {
+                throw new ConfigurationException("Configure OIDC only once 
using auth.oidc");
+            }
+            return serverConfiguration;
+        }
+        if (serverConfiguration.immutableConfigurationsAt("oidc").isEmpty()) {
+            return serverConfiguration;
+        }
+
+        BaseHierarchicalConfiguration normalized = new 
BaseHierarchicalConfiguration();
+        copyProperties(serverConfiguration, normalized, "");
+        copyProperties(serverConfiguration.configurationAt("oidc"), 
normalized, "auth.oidc.");
+        return normalized;
+    }
+
+    private static void 
copyProperties(HierarchicalConfiguration<ImmutableNode> source,
+                                       BaseHierarchicalConfiguration target,
+                                       String prefix) {
+        Iterator<String> keys = source.getKeys();
+        while (keys.hasNext()) {
+            String key = keys.next();
+            if (!prefix.isEmpty() || !key.startsWith("oidc.")) {
+                target.setProperty(prefix + key, source.getProperty(key));
+            }
+        }
+    }
 }
diff --git 
a/server/protocols/protocols-managesieve/src/main/resources/META-INF/spring/managesieveserver-context.xml
 
b/server/protocols/protocols-managesieve/src/main/resources/META-INF/spring/managesieveserver-context.xml
index ef9edadba9..1ceb71ab88 100644
--- 
a/server/protocols/protocols-managesieve/src/main/resources/META-INF/spring/managesieveserver-context.xml
+++ 
b/server/protocols/protocols-managesieve/src/main/resources/META-INF/spring/managesieveserver-context.xml
@@ -20,10 +20,14 @@
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd";>
 
     <bean id="managesieveserver" 
class="org.apache.james.managesieveserver.netty.ManageSieveServerFactory">
+        <constructor-arg index="0" ref="manageSieveSaslMechanismLoader"/>
         <property name="fileSystem" ref="filesystem"/>
         <property name="sieveRepository" ref="sieverepository"/>
-        <property name="usersRepository" ref="usersrepository"/>
+        <property name="authenticator" ref="authenticator"/>
         <property name="parser" ref="sieveparser"/>
     </bean>
 
+    <bean id="manageSieveSaslMechanismLoader" 
class="org.apache.james.managesieveserver.netty.ManageSieveServerFactory$ManageSieveSaslMechanismLoader"
+          factory-method="defaultLoader"/>
+
 </beans>
diff --git 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
index ef9b8e282e..f3b17d7456 100644
--- 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
+++ 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
@@ -26,25 +26,32 @@ import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.james.core.Username;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.filesystem.api.mock.MockFileSystem;
+import org.apache.james.mailbox.Authenticator;
+import org.apache.james.mailbox.Authorizator;
+import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.managesieve.core.CoreProcessor;
 import org.apache.james.managesieve.jsieve.Parser;
 import org.apache.james.managesieve.transcode.ArgumentParser;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
 import org.apache.james.managesieveserver.netty.ManageSieveServer;
+import 
org.apache.james.managesieveserver.netty.ManageSieveServerFactory.ManageSieveSaslMechanismLoader;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
 import org.apache.james.protocols.api.utils.ProtocolServerUtils;
 import org.apache.james.protocols.lib.LegacyJavaEncryptionFactory;
+import org.apache.james.protocols.sasl.JamesSaslAuthenticator;
 import org.apache.james.server.core.configuration.FileConfigurationProvider;
 import org.apache.james.sieverepository.file.SieveFileRepository;
+import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.memory.MemoryUsersRepository;
 
+import com.google.common.collect.ImmutableList;
+
 class ManageSieveServerTestSystem {
     private static final int MAX_LINE_LENGTH = 8000;
     private static final DomainList NO_DOMAIN_LIST = null;
     public static final String PASSWORD = "bobpwd";
     public static final Username USERNAME = Username.of("bob");
 
-
-    private ManageSieveProcessor manageSieveProcessor;
     public ManageSieveServer manageSieveServer;
     private MemoryUsersRepository usersRepository;
     private MockFileSystem fileSystem;
@@ -53,22 +60,34 @@ class ManageSieveServerTestSystem {
         this.usersRepository = 
MemoryUsersRepository.withoutVirtualHosting(NO_DOMAIN_LIST);
         this.usersRepository.addUser(USERNAME, PASSWORD);
         this.fileSystem = new MockFileSystem();
-        this.manageSieveProcessor = new ManageSieveProcessor(
-            new ArgumentParser(
-                new CoreProcessor(
-                    new SieveFileRepository(this.fileSystem),
-                    this.usersRepository,
-                    new Parser()
-                )
-            )
-        );
     }
 
     public void setUp(HierarchicalConfiguration<ImmutableNode> configuration) 
throws Exception {
+        ImmutableList<SaslMechanism> saslMechanisms = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration);
+        setUp(configuration, saslMechanisms);
+    }
+
+    public void setUp(ImmutableList<SaslMechanism> saslMechanisms) throws 
Exception {
+        HierarchicalConfiguration<ImmutableNode> configuration = 
FileConfigurationProvider.getConfig(ClassLoader.getSystemResourceAsStream("managesieveserver.xml"));
+        setUp(configuration, saslMechanisms);
+    }
+
+    private void setUp(HierarchicalConfiguration<ImmutableNode> configuration, 
ImmutableList<SaslMechanism> saslMechanisms) throws Exception {
         this.fileSystem.clear();
+        Authenticator authenticator = (username, password) -> {
+            try {
+                return usersRepository.test(username, password.toString());
+            } catch (UsersRepositoryException e) {
+                throw new MailboxException("Unable to access users 
repository", e);
+            }
+        };
+        ManageSieveProcessor manageSieveProcessor = new ManageSieveProcessor(
+            new ArgumentParser(new CoreProcessor(new 
SieveFileRepository(this.fileSystem), new Parser(), saslMechanisms)),
+            saslMechanisms,
+            new JamesSaslAuthenticator(authenticator, (user, otherUser) -> 
Authorizator.AuthorizationState.FORBIDDEN));
         this.manageSieveServer = new ManageSieveServer(
             MAX_LINE_LENGTH,
-            this.manageSieveProcessor
+            manageSieveProcessor
         );
         this.manageSieveServer.setFileSystem(this.fileSystem);
         this.manageSieveServer.setEncryptionFactory(new 
LegacyJavaEncryptionFactory(this.fileSystem));
diff --git 
a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactoryTest.java
 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactoryTest.java
new file mode 100644
index 0000000000..f0485269c4
--- /dev/null
+++ 
b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/netty/ManageSieveServerFactoryTest.java
@@ -0,0 +1,138 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.managesieveserver.netty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import 
org.apache.james.managesieveserver.netty.ManageSieveServerFactory.ManageSieveSaslMechanismLoader;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslMechanismNames;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+
+class ManageSieveServerFactoryTest {
+    private static final String JWKS_URL = "https://example.com/oidc/jwks";;
+    private static final String OIDC_CONFIGURATION_URL = 
"https://example.com/.well-known/openid-configuration";;
+
+    @Test
+    void defaultLoaderShouldEnableOnlyPlainWithoutOidcConfiguration() throws 
Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+
+        ImmutableList<SaslMechanism> mechanisms = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration);
+
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly(SaslMechanismNames.PLAIN);
+    }
+
+    @Test
+    void defaultPlainMechanismShouldPreserveClearTextAuthentication() throws 
Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+
+        SaslMechanism plain = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration).getFirst();
+
+        assertThat(plain.isAvailableOnTransport(false)).isTrue();
+    }
+
+    @Test
+    void defaultPlainMechanismShouldHonorRequireSsl() throws Exception {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.requireSSL", true);
+
+        SaslMechanism plain = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration).getFirst();
+
+        assertThat(plain.isAvailableOnTransport(false)).isFalse();
+        assertThat(plain.isAvailableOnTransport(true)).isTrue();
+    }
+
+    @Test
+    void legacyOidcConfigurationShouldEnableSharedOauthMechanisms() throws 
Exception {
+        BaseHierarchicalConfiguration configuration = 
oidcConfiguration("oidc.");
+
+        ImmutableList<SaslMechanism> mechanisms = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration);
+
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly(SaslMechanismNames.PLAIN, 
SaslMechanismNames.OAUTHBEARER, SaslMechanismNames.XOAUTH2);
+    }
+
+    @Test
+    void defaultLoaderShouldPreserveExplicitBuiltInFactoryOrder() throws 
Exception {
+        BaseHierarchicalConfiguration configuration = 
oidcConfiguration("auth.oidc.");
+        configuration.addProperty("auth.saslMechanisms", 
"XOauth2SaslMechanismFactory,PlainSaslMechanismFactory");
+
+        ImmutableList<SaslMechanism> mechanisms = 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration);
+
+        assertThat(mechanisms)
+            .extracting(SaslMechanism::name)
+            .containsExactly(SaslMechanismNames.XOAUTH2, 
SaslMechanismNames.PLAIN);
+    }
+
+    @Test
+    void defaultLoaderShouldRejectExternalFactory() {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty("auth.saslMechanisms", 
"com.example.CustomSaslMechanismFactory");
+
+        assertThatThrownBy(() -> 
ManageSieveSaslMechanismLoader.defaultLoader().load(configuration))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessageContaining("Unsupported SASL mechanism factory");
+    }
+
+    @Test
+    void normalizationShouldPreserveServerPropertiesWithoutMutatingSource() 
throws Exception {
+        BaseHierarchicalConfiguration configuration = 
oidcConfiguration("oidc.");
+        configuration.addProperty("bind", "127.0.0.1:4190");
+
+        HierarchicalConfiguration<ImmutableNode> normalized = 
ManageSieveServerFactory.normalizeSaslConfiguration(configuration);
+        HierarchicalConfiguration<ImmutableNode> normalizedAgain = 
ManageSieveServerFactory.normalizeSaslConfiguration(normalized);
+
+        assertThat(normalized.getString("bind")).isEqualTo("127.0.0.1:4190");
+        
assertThat(normalized.getString("auth.oidc.jwksURL")).isEqualTo(JWKS_URL);
+        assertThat(normalized.containsKey("oidc.jwksURL")).isFalse();
+        
assertThat(normalizedAgain.getString("auth.oidc.jwksURL")).isEqualTo(JWKS_URL);
+        assertThat(configuration.containsKey("auth.oidc.jwksURL")).isFalse();
+    }
+
+    @Test
+    void normalizationShouldRejectBothOidcConfigurationPaths() {
+        BaseHierarchicalConfiguration configuration = 
oidcConfiguration("oidc.");
+        configuration.addProperty("auth.oidc.jwksURL", JWKS_URL);
+
+        assertThatThrownBy(() -> 
ManageSieveServerFactory.normalizeSaslConfiguration(configuration))
+            .isInstanceOf(ConfigurationException.class)
+            .hasMessageContaining("Configure OIDC only once");
+    }
+
+    private static BaseHierarchicalConfiguration oidcConfiguration(String 
prefix) {
+        BaseHierarchicalConfiguration configuration = new 
BaseHierarchicalConfiguration();
+        configuration.addProperty(prefix + "jwksURL", JWKS_URL);
+        configuration.addProperty(prefix + "claim", "email");
+        configuration.addProperty(prefix + "oidcConfigurationURL", 
OIDC_CONFIGURATION_URL);
+        configuration.addProperty(prefix + "scope", "email");
+        configuration.addProperty(prefix + "aud", "manage-sieve");
+        return configuration;
+    }
+}
diff --git 
a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
 
b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
index 9ed26d0140..14028338f2 100644
--- 
a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
+++ 
b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
@@ -7,14 +7,16 @@
     <connectionLimit>0</connectionLimit>
     <connectionLimitPerIP>0</connectionLimitPerIP>
 
-    <oidc>
-        
<jwksURL>http://127.0.0.1/realms/test/protocol/openid-connect/certs</jwksURL>
-        <claim>sub</claim>
-        
<oidcConfigurationURL>https://127.0.0.1/realms/test/.well-known/openid-configuration</oidcConfigurationURL>
-        <scope>email</scope>
-        <introspection>
-            <url>https://127.0.0.1/oidc/introspect</url>
-        </introspection>
-        <aud>james</aud>
-    </oidc>
+    <auth>
+        <oidc>
+            
<jwksURL>http://127.0.0.1/realms/test/protocol/openid-connect/certs</jwksURL>
+            <claim>sub</claim>
+            
<oidcConfigurationURL>https://127.0.0.1/realms/test/.well-known/openid-configuration</oidcConfigurationURL>
+            <scope>email</scope>
+            <introspection>
+                <url>https://127.0.0.1/oidc/introspect</url>
+            </introspection>
+            <aud>james</aud>
+        </oidc>
+    </auth>
 </managesieveserver>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to