This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit ba6abe993d9829fc05c0beee1d48d8e829eb7a67 Author: Quan Tran <[email protected]> AuthorDate: Wed Jun 24 14:20:21 2026 +0700 JAMES-4210 Wire SMTP SASL support for Spring and scaling Pulsar apps Adapt non-Guice and Spring SMTP server creation to provide default SASL mechanisms and authenticator wiring. Add a UsersRepository-backed authenticator for the scaling Pulsar SMTP app. --- .../src/main/java/org/apache/james/Main.java | 2 + .../james/UsersRepositoryBackedAuthenticator.java | 48 +++++++++++++ server/protocols/protocols-smtp/pom.xml | 4 ++ .../apache/james/smtpserver/netty/SMTPServer.java | 83 ++++++++-------------- .../james/smtpserver/netty/SMTPServerFactory.java | 55 +++++++++++++- .../META-INF/spring/smtpserver-context.xml | 10 +++ 6 files changed, 146 insertions(+), 56 deletions(-) diff --git a/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/Main.java b/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/Main.java index 7fc3a55792..8c08ae690a 100644 --- a/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/Main.java +++ b/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/Main.java @@ -25,6 +25,7 @@ import org.apache.james.blob.api.BlobStoreDAO; import org.apache.james.blob.api.MetricableBlobStore; import org.apache.james.blob.objectstorage.aws.S3BlobStoreDAO; import org.apache.james.blob.objectstorage.aws.S3RequestOption; +import org.apache.james.mailbox.Authenticator; import org.apache.james.mailrepository.api.MailRepositoryFactory; import org.apache.james.mailrepository.api.MailRepositoryUrlStore; import org.apache.james.mailrepository.postgres.PostgresMailRepositoryFactory; @@ -109,6 +110,7 @@ public class Main implements JamesServerMain { Multibinder.newSetBinder(binder, PostgresDataDefinition.class) .addBinding().toInstance(org.apache.james.mailrepository.postgres.PostgresMailRepositoryDataDefinition.MODULE); binder.bind(MailRepositoryUrlStore.class).to(PostgresMailRepositoryUrlStore.class).in(Scopes.SINGLETON); + binder.bind(Authenticator.class).to(UsersRepositoryBackedAuthenticator.class).in(Scopes.SINGLETON); }, new CoreDataModule(), new MemoryDelegationStoreModule(), diff --git a/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/UsersRepositoryBackedAuthenticator.java b/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/UsersRepositoryBackedAuthenticator.java new file mode 100644 index 0000000000..f980601e08 --- /dev/null +++ b/server/apps/scaling-pulsar-smtp/src/main/java/org/apache/james/UsersRepositoryBackedAuthenticator.java @@ -0,0 +1,48 @@ +/**************************************************************** + * 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; + +import java.util.Optional; + +import jakarta.inject.Inject; + +import org.apache.james.core.Username; +import org.apache.james.mailbox.Authenticator; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.user.api.UsersRepository; +import org.apache.james.user.api.UsersRepositoryException; + +class UsersRepositoryBackedAuthenticator implements Authenticator { + private final UsersRepository usersRepository; + + @Inject + UsersRepositoryBackedAuthenticator(UsersRepository usersRepository) { + this.usersRepository = usersRepository; + } + + @Override + public Optional<Username> isAuthentic(Username userid, CharSequence passwd) throws MailboxException { + try { + return usersRepository.test(userid, passwd.toString()); + } catch (UsersRepositoryException e) { + throw new MailboxException("Unable to access UsersRepository", e); + } + } +} diff --git a/server/protocols/protocols-smtp/pom.xml b/server/protocols/protocols-smtp/pom.xml index 5351f7b5ac..d8d790af5d 100644 --- a/server/protocols/protocols-smtp/pom.xml +++ b/server/protocols/protocols-smtp/pom.xml @@ -142,6 +142,10 @@ <groupId>${james.protocols.groupId}</groupId> <artifactId>protocols-netty</artifactId> </dependency> + <dependency> + <groupId>${james.protocols.groupId}</groupId> + <artifactId>protocols-sasl</artifactId> + </dependency> <dependency> <groupId>${james.protocols.groupId}</groupId> diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java index e509ac384c..b60abaa58c 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java @@ -24,9 +24,7 @@ import static org.apache.james.smtpserver.netty.SMTPServer.AuthenticationAnnounc import static org.apache.james.smtpserver.netty.SMTPServer.AuthenticationAnnounceMode.NEVER; import java.net.InetSocketAddress; -import java.net.MalformedURLException; import java.net.SocketAddress; -import java.net.URISyntaxException; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -44,9 +42,10 @@ import org.apache.james.core.Disconnector; import org.apache.james.core.Username; import org.apache.james.dnsservice.api.DNSService; import org.apache.james.dnsservice.library.netmatcher.NetMatcher; -import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolSession; import org.apache.james.protocols.api.ProtocolTransport; +import org.apache.james.protocols.api.sasl.SaslAuthenticator; +import org.apache.james.protocols.api.sasl.SaslMechanism; import org.apache.james.protocols.lib.handler.HandlersPackage; import org.apache.james.protocols.lib.netty.AbstractProtocolAsyncServer; import org.apache.james.protocols.netty.AbstractChannelPipelineFactory; @@ -55,6 +54,7 @@ import org.apache.james.protocols.netty.ChannelHandlerFactory; import org.apache.james.protocols.smtp.SMTPConfiguration; import org.apache.james.protocols.smtp.SMTPProtocol; import org.apache.james.protocols.smtp.SMTPSession; +import org.apache.james.protocols.smtp.core.esmtp.AuthCmdHandler; import org.apache.james.smtpserver.CoreCmdHandlerLoader; import org.apache.james.smtpserver.ExtendedSMTPSession; import org.apache.james.smtpserver.jmx.JMXHandlersLoader; @@ -62,6 +62,7 @@ import org.apache.james.util.Size; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -110,30 +111,15 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe } } - public static class AuthenticationConfiguration { - private static final String OIDC_PATH = "auth.oidc"; + public static class AuthAnnouncementConfiguration { + public static final boolean REQUIRE_SSL_DEFAULT = false; - public static AuthenticationConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException { - return new AuthenticationConfiguration( + public static AuthAnnouncementConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException { + return new AuthAnnouncementConfiguration( Optional.ofNullable(configuration.getString("auth.announce", null)) .map(AuthenticationAnnounceMode::parse) .orElseGet(() -> fallbackAuthenticationAnnounceMode(configuration)), - configuration.getBoolean("auth.requireSSL", false), - configuration.getBoolean("auth.plainAuthEnabled", true), - parseSASLConfiguration(configuration)); - } - - private static Optional<OidcSASLConfiguration> parseSASLConfiguration(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException { - boolean haveOidcProperties = configuration.getKeys(OIDC_PATH).hasNext(); - if (haveOidcProperties) { - try { - return Optional.of(OidcSASLConfiguration.parse(configuration.configurationAt(OIDC_PATH))); - } catch (MalformedURLException | URISyntaxException exception) { - throw new ConfigurationException("Failed to retrieve oauth component", exception); - } - } else { - return Optional.empty(); - } + configuration.getBoolean("auth.requireSSL", REQUIRE_SSL_DEFAULT)); } private static AuthenticationAnnounceMode fallbackAuthenticationAnnounceMode(HierarchicalConfiguration<ImmutableNode> configuration) { @@ -142,37 +128,22 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe private final AuthenticationAnnounceMode authenticationAnnounceMode; private final boolean requireSSL; - private final boolean plainAuthEnabled; - private final Optional<OidcSASLConfiguration> saslConfiguration; - public AuthenticationConfiguration(AuthenticationAnnounceMode authenticationAnnounceMode, boolean requireSSL, boolean plainAuthEnabled, Optional<OidcSASLConfiguration> saslConfiguration) { + public AuthAnnouncementConfiguration(AuthenticationAnnounceMode authenticationAnnounceMode, boolean requireSSL) { this.authenticationAnnounceMode = authenticationAnnounceMode; this.requireSSL = requireSSL; - this.plainAuthEnabled = plainAuthEnabled; - this.saslConfiguration = saslConfiguration; } public AuthenticationAnnounceMode getAuthenticationAnnounceMode() { return authenticationAnnounceMode; } - public boolean isRequireSSL() { - return requireSSL; - } - - public boolean isPlainAuthEnabled() { - return plainAuthEnabled; - } - - public Optional<OidcSASLConfiguration> getSaslConfiguration() { - return saslConfiguration; - } } /** * Whether authentication is required to use this SMTP server. */ - private AuthenticationConfiguration authenticationConfiguration; + private AuthAnnouncementConfiguration authAnnouncementConfiguration; /** * Whether the server needs helo to be send first @@ -203,6 +174,8 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe private final SmtpMetrics smtpMetrics; private final DefaultChannelGroup smtpChannelGroup; private Set<String> disabledFeatures = ImmutableSet.of(); + private ImmutableList<SaslMechanism> saslMechanisms = ImmutableList.of(); + private Optional<SaslAuthenticator> saslAuthenticator = Optional.empty(); private boolean addressBracketsEnforcement = true; @@ -216,6 +189,14 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe this.smtpChannelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); } + public void setSaslMechanisms(ImmutableList<SaslMechanism> saslMechanisms) { + this.saslMechanisms = saslMechanisms; + } + + public void setSaslAuthenticator(Optional<SaslAuthenticator> saslAuthenticator) { + this.saslAuthenticator = saslAuthenticator; + } + @Inject public void setDnsService(DNSService dns) { this.dns = dns; @@ -224,6 +205,9 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe @Override protected void preInit() throws Exception { super.preInit(); + saslAuthenticator.ifPresent(authenticator -> getProtocolHandlerChain() + .getHandlers(AuthCmdHandler.class) + .forEach(handler -> handler.configureSaslMechanisms(saslMechanisms, authenticator))); if (authorizedAddresses != null) { java.util.StringTokenizer st = new java.util.StringTokenizer(authorizedAddresses, ", ", false); java.util.Collection<String> networks = new java.util.ArrayList<>(); @@ -246,7 +230,7 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe public void doConfigure(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException { super.doConfigure(configuration); if (isEnabled()) { - authenticationConfiguration = AuthenticationConfiguration.parse(configuration); + authAnnouncementConfiguration = AuthAnnouncementConfiguration.parse(configuration); authorizedAddresses = configuration.getString("authorizedAddresses", null); @@ -322,19 +306,15 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe return SMTPServer.this.addressBracketsEnforcement; } - public boolean isPlainAuthEnabled() { - return authenticationConfiguration.isPlainAuthEnabled(); - } - @Override public boolean isAuthAnnounced(String remoteIP, boolean tlsStarted) { - if (authenticationConfiguration.requireSSL && !tlsStarted) { + if (authAnnouncementConfiguration.requireSSL && !tlsStarted) { return false; } - if (authenticationConfiguration.getAuthenticationAnnounceMode() == ALWAYS) { + if (authAnnouncementConfiguration.getAuthenticationAnnounceMode() == ALWAYS) { return true; } - if (authenticationConfiguration.getAuthenticationAnnounceMode() == NEVER) { + if (authAnnouncementConfiguration.getAuthenticationAnnounceMode() == NEVER) { return false; } return Optional.ofNullable(authorizedNetworks) @@ -356,11 +336,6 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe return "JAMES SMTP Server "; } - @Override - public Optional<OidcSASLConfiguration> saslConfiguration() { - return authenticationConfiguration.getSaslConfiguration(); - } - @Override public Set<String> disabledFeatures() { return disabledFeatures; @@ -428,7 +403,7 @@ public class SMTPServer extends AbstractProtocolAsyncServer implements SMTPServe } public AuthenticationAnnounceMode getAuthRequired() { - return authenticationConfiguration.getAuthenticationAnnounceMode(); + return authAnnouncementConfiguration.getAuthenticationAnnounceMode(); } @Override diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServerFactory.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServerFactory.java index 5d4bbc08f4..c04861508e 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServerFactory.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServerFactory.java @@ -21,12 +21,14 @@ package org.apache.james.smtpserver.netty; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; import jakarta.inject.Inject; import org.apache.commons.configuration2.HierarchicalConfiguration; +import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.james.core.ConnectionDescription; import org.apache.james.core.ConnectionDescriptionSupplier; @@ -35,26 +37,73 @@ import org.apache.james.core.Username; import org.apache.james.dnsservice.api.DNSService; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.metrics.api.MetricFactory; +import org.apache.james.protocols.api.sasl.SaslAuthenticator; +import org.apache.james.protocols.api.sasl.SaslMechanism; +import org.apache.james.protocols.api.sasl.SaslMechanismFactory; import org.apache.james.protocols.lib.handler.ProtocolHandlerLoader; 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.OauthBearerSaslMechanismFactory; +import org.apache.james.protocols.sasl.PlainSaslMechanismFactory; +import org.apache.james.protocols.sasl.XOauth2SaslMechanismFactory; +import org.apache.james.smtpserver.netty.SMTPServer.AuthAnnouncementConfiguration; + +import com.github.fge.lambdas.Throwing; +import com.google.common.collect.ImmutableList; public class SMTPServerFactory extends AbstractServerFactory implements Disconnector, ConnectionDescriptionSupplier { + @FunctionalInterface + public interface SmtpSaslMechanismLoader { + static SmtpSaslMechanismLoader defaultLoader() { + ImmutableList<SaslMechanismFactory> defaultFactories = ImmutableList.of( + new PlainSaslMechanismFactory(AuthAnnouncementConfiguration.REQUIRE_SSL_DEFAULT), + new OauthBearerSaslMechanismFactory(), + new XOauth2SaslMechanismFactory()); + return configuration -> loadBuiltInMechanisms(defaultFactories, configuration); + } + + ImmutableList<SaslMechanism> load(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException; + } + + private static ImmutableList<SaslMechanism> loadBuiltInMechanisms(ImmutableList<SaslMechanismFactory> defaultFactories, + HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException { + try { + return BuiltInSaslMechanismFactories.enabledForServer(defaultFactories, configuration) + .stream() + .map(Throwing.function(factory -> factory.create(configuration))) + .collect(ImmutableList.toImmutableList()); + } catch (RuntimeException e) { + if (e.getCause() instanceof ConfigurationException configurationException) { + throw configurationException; + } + throw e; + } + } protected final DNSService dns; protected final ProtocolHandlerLoader loader; protected final FileSystem fileSystem; protected final SmtpMetricsImpl smtpMetrics; + protected final SmtpSaslMechanismLoader saslMechanismLoader; + protected final Optional<SaslAuthenticator> saslAuthenticator; protected Encryption.Factory encryptionFactory; - @Inject public SMTPServerFactory(DNSService dns, ProtocolHandlerLoader loader, FileSystem fileSystem, - MetricFactory metricFactory) { + MetricFactory metricFactory, SmtpSaslMechanismLoader saslMechanismLoader, SaslAuthenticator saslAuthenticator) { + this(dns, loader, fileSystem, metricFactory, saslMechanismLoader, Optional.of(saslAuthenticator)); + } + + private SMTPServerFactory(DNSService dns, ProtocolHandlerLoader loader, FileSystem fileSystem, + MetricFactory metricFactory, SmtpSaslMechanismLoader saslMechanismLoader, + Optional<SaslAuthenticator> saslAuthenticator) { this.dns = dns; this.loader = loader; this.fileSystem = fileSystem; this.smtpMetrics = new SmtpMetricsImpl(metricFactory); + this.saslMechanismLoader = saslMechanismLoader; + this.saslAuthenticator = saslAuthenticator; } @Inject @@ -78,6 +127,8 @@ public class SMTPServerFactory extends AbstractServerFactory implements Disconne server.setProtocolHandlerLoader(loader); server.setFileSystem(fileSystem); server.setEncryptionFactory(encryptionFactory); + server.setSaslMechanisms(saslMechanismLoader.load(serverConfig)); + server.setSaslAuthenticator(saslAuthenticator); server.configure(serverConfig); servers.add(server); } diff --git a/server/protocols/protocols-smtp/src/main/resources/META-INF/spring/smtpserver-context.xml b/server/protocols/protocols-smtp/src/main/resources/META-INF/spring/smtpserver-context.xml index 68fedd9e1c..1ac134715b 100644 --- a/server/protocols/protocols-smtp/src/main/resources/META-INF/spring/smtpserver-context.xml +++ b/server/protocols/protocols-smtp/src/main/resources/META-INF/spring/smtpserver-context.xml @@ -23,6 +23,16 @@ <constructor-arg index="1" ref="protocolhandlerloader"/> <constructor-arg index="2" ref="filesystem"/> <constructor-arg index="3" ref="metricFactory"/> + <constructor-arg index="4" ref="smtpSaslMechanismLoader"/> + <constructor-arg index="5" ref="smtpSaslAuthenticator"/> + </bean> + + <bean id="smtpSaslMechanismLoader" class="org.apache.james.smtpserver.netty.SMTPServerFactory$SmtpSaslMechanismLoader" + factory-method="defaultLoader"/> + + <bean id="smtpSaslAuthenticator" class="org.apache.james.protocols.sasl.JamesSaslAuthenticator"> + <constructor-arg index="0" ref="authenticator"/> + <constructor-arg index="1" ref="authorizator"/> </bean> </beans> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
