Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ public interface SMTPSession extends ProtocolSession {
/** HELO or EHLO */
AttachmentKey<String> CURRENT_HELO_MODE = AttachmentKey.of("CURRENT_HELO_MODE", String.class);
AttachmentKey<String> CURRENT_HELO_NAME = AttachmentKey.of("CURRENT_HELO_NAME", String.class);
/** Set when the authenticated account was granted the right to use identities other than its own */
AttachmentKey<Boolean> ALLOW_USE_OTHER_IDENTITY = AttachmentKey.of("ALLOW_USE_OTHER_IDENTITY", Boolean.class);

/**
* Whether this session is allowed to use MAIL FROM / From identities other than the one it authenticated with.
*
* Set upon authentication for accounts explicitly granted that right, this bypasses the identity checks
* performed by the sender identity verification hook (see the <code>verifyIdentity</code> setting).
*
* @return true if identity verification is to be bypassed for this session
*/
default boolean allowUseOtherIdentity() {
return getAttachment(ALLOW_USE_OTHER_IDENTITY, State.Connection).orElse(false);
}

/**
* Returns the service wide configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ protected HookResult doCheck(SMTPSession session, MaybeSender sender) {

@Override
public HookResult doMail(SMTPSession session, MaybeSender sender) {
if (session.allowUseOtherIdentity()) {
return HookResult.DECLINED;
}
return doCheck(session, sender);
}

Expand All @@ -97,6 +100,9 @@ public HookResult doMail(SMTPSession session, MaybeSender sender) {
*/
@Override
public HookResult doRcpt(SMTPSession session, MaybeSender sender, MailAddress rcpt) {
if (session.allowUseOtherIdentity()) {
return HookResult.DECLINED;
}
return doCheck(session, sender);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.commons.lang3.NotImplementedException;
import org.apache.james.core.Username;
import org.apache.james.jwt.OidcSASLConfiguration;
import org.apache.james.protocols.api.ProtocolSession;
import org.apache.james.protocols.smtp.SMTPSession;
import org.apache.james.protocols.smtp.hook.AuthHook;
import org.apache.james.protocols.smtp.hook.HookResult;
Expand All @@ -38,20 +39,30 @@
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Multimap;

/**
* Declarative authentication.
*
* <p>Each {@code account} supports an optional {@code allowUseOtherIdentity} flag (defaults to {@code false}).
* When set, the authenticated session bypasses the {@code verifyIdentity} checks and may thus use any
* MAIL FROM / From identity. This is intended for application accounts sending on behalf of end users
* (calendar invitations, notifications...), and should be granted only to accounts whose credentials are
* under the operator control.</p>
*
* @deprecated Prefer implementing a SASL mechanism factory. Existing handler-chain registrations
* are adapted by the SMTP AUTH handler during migration.
*/
@Deprecated
public class ConfigurationAuthHook implements AuthHook {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationAuthHook.class);

private Multimap<Username, String> accounts = ImmutableListMultimap.of();
private record Account(Username username, List<String> passwords, boolean allowUseOtherIdentity) {
boolean matches(Username username, String password) {
return this.username.equals(username) && passwords.stream().anyMatch(password::equals);
}
}

private List<Account> accounts = ImmutableList.of();

@Inject
public ConfigurationAuthHook() {
Expand All @@ -62,40 +73,44 @@ public ConfigurationAuthHook() {
public void init(Configuration config) throws ConfigurationException {
HierarchicalConfiguration<ImmutableNode> hierarchicalConfiguration = (HierarchicalConfiguration<ImmutableNode>) config;

ImmutableListMultimap.Builder<Username, String> builder = ImmutableListMultimap.builder();

for (HierarchicalConfiguration<ImmutableNode> accountNode : hierarchicalConfiguration.configurationAt("accounts")
.configurationsAt("account")) {
String username = accountNode.getString("username");
if (username != null) {
List<String> passwords = accountNode.getList(String.class, "passwords.password");
passwords.forEach(pw -> builder.put(Username.of(username), pw));
}
}
this.accounts = builder.build();

LOGGER.info("SMTP authentication enabled from configuration for users: {}", accounts.keySet()
this.accounts = hierarchicalConfiguration.configurationAt("accounts")
.configurationsAt("account")
.stream()
.map(Username::asString)
.flatMap(accountNode -> parseAccount(accountNode).stream())
.collect(ImmutableList.toImmutableList());

LOGGER.info("SMTP authentication enabled from configuration for users: {}", accounts.stream()
.map(account -> account.username().asString())
.collect(ImmutableList.toImmutableList()));
}

private Optional<Account> parseAccount(HierarchicalConfiguration<ImmutableNode> accountNode) {
return Optional.ofNullable(accountNode.getString("username"))
.map(username -> new Account(Username.of(username),
accountNode.getList(String.class, "passwords.password", ImmutableList.of()),
accountNode.getBoolean("allowUseOtherIdentity", false)));
}

@Override
public HookResult doAuth(SMTPSession session, Username username, String password) {
Optional<Username> loggedInUser = Optional.ofNullable(accounts.get(username))
.filter(allowedsPass -> allowedsPass.stream().anyMatch(password::equals))
.map(any -> username);

if (loggedInUser.isPresent()) {
session.setUsername(loggedInUser.get());
session.setRelayingAllowed(true);

return HookResult.builder()
.hookReturnCode(HookReturnCode.ok())
.smtpDescription("Authentication Successful")
.build();
return accounts.stream()
.filter(account -> account.matches(username, password))
.findFirst()
.map(account -> authenticate(session, account))
.orElse(HookResult.DECLINED);
}

private HookResult authenticate(SMTPSession session, Account account) {
session.setUsername(account.username());
session.setRelayingAllowed(true);
if (account.allowUseOtherIdentity()) {
session.setAttachment(SMTPSession.ALLOW_USE_OTHER_IDENTITY, true, ProtocolSession.State.Connection);
}
return HookResult.DECLINED;

return HookResult.builder()
.hookReturnCode(HookReturnCode.ok())
.smtpDescription("Authentication Successful")
.build();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ protected boolean isSenderAllowed(Username connectedUser, Username sender) {

@Override
public HookResult onMessage(SMTPSession session, Mail mail) {
if (session.allowUseOtherIdentity()) {
return HookResult.DECLINED;
}
ExtendedSMTPSession nSession = (ExtendedSMTPSession) session;
boolean shouldCheck = nSession.senderVerificationConfiguration().mode() == SMTPConfiguration.SenderVerificationMode.STRICT ||
(nSession.senderVerificationConfiguration().mode() == SMTPConfiguration.SenderVerificationMode.RELAXED && session.getUsername() != null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/****************************************************************
* 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.smtpserver;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;

import java.net.InetSocketAddress;
import java.util.Base64;

import org.apache.commons.net.smtp.SMTPClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ConfiguredAuthOtherIdentityTest {
private static final String END_USER = "bob@example.local";

private final SMTPServerTestSystem smtpServerTestSystem = new SMTPServerTestSystem();

@BeforeEach
void setUp() throws Exception {
smtpServerTestSystem.setUp("smtpserver-configured-auth-other-identity.xml");
}

@AfterEach
void tearDown() {
smtpServerTestSystem.smtpServer.destroy();
}

private SMTPClient authenticate(String username, String password) throws Exception {
SMTPClient smtpProtocol = new SMTPClient();
InetSocketAddress bindedAddress = smtpServerTestSystem.getBindedAddress();
smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort());

smtpProtocol.sendCommand("AUTH PLAIN");
smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + username + "\0" + password + "\0").getBytes(UTF_8)));
assertThat(smtpProtocol.getReplyCode())
.as("authenticated")
.isEqualTo(235);
smtpProtocol.login("domain.tld");
return smtpProtocol;
}

@Test
void mailFromOtherIdentityShouldBeRejectedWhenNotAllowed() throws Exception {
SMTPClient smtpProtocol = authenticate("noreply-tdrive@domain.tld", "secret123456");

smtpProtocol.setSender(END_USER);

assertThat(smtpProtocol.getReplyCode())
.isEqualTo(503);
}

@Test
void mailFromOtherIdentityShouldBeAcceptedWhenAllowed() throws Exception {
SMTPClient smtpProtocol = authenticate("noreply-tcalendar@domain.tld", "secret234567");

smtpProtocol.setSender(END_USER);

assertThat(smtpProtocol.getReplyCode())
.isEqualTo(250);
}

@Test
void headerFromOtherIdentityShouldBeAcceptedWhenAllowed() throws Exception {
SMTPClient smtpProtocol = authenticate("noreply-tcalendar@domain.tld", "secret234567");

smtpProtocol.setSender(END_USER);
smtpProtocol.addRecipient("mail@sample.com");
smtpProtocol.sendShortMessageData("From: " + END_USER + "\r\nSubject: test\r\n\r\nTest body\r\n.\r\n");
smtpProtocol.quit();

assertThat(smtpServerTestSystem.queue.getLastMail().getMaybeSender().asString())
.isEqualTo(END_USER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?xml version="1.0"?>

<!--
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.
-->

<!-- Read https://james.apache.org/server/config-smtp-lmtp.html#SMTP_Configuration for further details -->

<smtpserver enabled="true">
<bind>0.0.0.0:0</bind>
<connectionBacklog>200</connectionBacklog>
<tls socketTLS="false" startTLS="false">
<keystore>file://conf/keystore</keystore>
<secret>james72laBalle</secret>
<provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
<algorithm>SunX509</algorithm>
</tls>
<connectiontimeout>360</connectiontimeout>
<connectionLimit>0</connectionLimit>
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>forUnauthorizedAddresses</announce>
<requireSSL>false</requireSSL>
</auth>
<verifyIdentity>strict</verifyIdentity>
<maxmessagesize>0</maxmessagesize>
<addressBracketsEnforcement>true</addressBracketsEnforcement>
<smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
<handlerchain coreHandlersPackage="org.apache.james.smtpserver.NoAuthCmdHandlerLoader" enableJmx="false">
<handler class="org.apache.james.protocols.smtp.core.esmtp.AuthCmdHandler" />
<handler class="org.apache.james.smtpserver.SetMailAttributeMessageHook" >
<name>technicaluser</name>
<value>true</value>
</handler>
<handler class="org.apache.james.smtpserver.ConfigurationAuthHook" >
<accounts>
<account>
<username>noreply-tdrive@domain.tld</username>
<passwords>
<password>secret123456</password>
<password>here_to_ease_secret_rotation</password>
<password>here_to_give_different_creds_to_each_app</password>
</passwords>
</account>
<!-- Allowed to send on behalf of end users: verifyIdentity is bypassed for this account. -->
<account>
<username>noreply-tcalendar@domain.tld</username>
<passwords>
<password>secret234567</password>
</passwords>
<allowUseOtherIdentity>true</allowUseOtherIdentity>
</account>
</accounts>
</handler>
</handlerchain>
<gracefulShutdown>false</gracefulShutdown>
<disabledFeatures>ENHANCEDSTATUSCODES</disabledFeatures>
</smtpserver>
5 changes: 5 additions & 0 deletions src/site/xdoc/server/config-smtp-lmtp.xml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@
<li><code>true</code>: act as <code>strict</code></li>
<li><code>false</code>: act as <code>disabled</code></li>
</ul>

Note that individual accounts declared on the <code>ConfigurationAuthHook</code> handler can be granted
<code>allowUseOtherIdentity</code> (defaults to <code>false</code>), which bypasses these checks for the
sessions they authenticate. This allows application accounts to send on behalf of end users
(calendar invitations, notifications...) without turning identity verification off for everybody else.
</dd>
<dt><strong>handler.maxmessagesize</strong></dt>
<dd>This is an optional tag with a non-negative integer body. It specifies the maximum
Expand Down