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]
