You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by hq...@apache.org on 2026/08/18 07:53:19 UTC
(james-project) 02/12: JAMES-4210 Drive ManageSieve AUTHENTICATE through SASL exchanges
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 26824b5b4b5fa42d07ff11de745e594acfc183d5
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Thu Aug 13 09:42:11 2026 +0700
JAMES-4210 Drive ManageSieve AUTHENTICATE through SASL exchanges
Replace the mechanism-specific ManageSieve authentication processors with a protocol-local SASL exchange driver.
---
protocols/managesieve/pom.xml | 16 --
.../managesieve/api/AuthenticationProcessor.java | 34 ---
.../org/apache/james/managesieve/api/Session.java | 17 +-
.../managesieve/api/UnknownSaslMechanism.java | 28 ---
.../managesieve/api/commands/Authenticate.java | 48 ----
.../managesieve/api/commands/CoreCommands.java | 2 +-
.../managesieve/api/commands/LineCommands.java | 2 +-
.../james/managesieve/core/CoreProcessor.java | 93 ++------
.../core/OAUTHAuthenticationProcessor.java | 75 -------
.../core/PlainAuthenticationProcessor.java | 94 --------
.../managesieve/transcode/ArgumentParser.java | 11 -
.../transcode/ManageSieveProcessor.java | 93 +++-----
.../transcode/ManageSieveSaslProcessor.java | 248 +++++++++++++++++++++
.../james/managesieve/util/SettableSession.java | 30 ++-
14 files changed, 330 insertions(+), 461 deletions(-)
diff --git a/protocols/managesieve/pom.xml b/protocols/managesieve/pom.xml
index 5522b6fa88..01c3b1a869 100644
--- a/protocols/managesieve/pom.xml
+++ b/protocols/managesieve/pom.xml
@@ -37,14 +37,6 @@
<groupId>${james.groupId}</groupId>
<artifactId>apache-jsieve-core</artifactId>
</dependency>
- <dependency>
- <groupId>${james.groupId}</groupId>
- <artifactId>james-server-data-api</artifactId>
- </dependency>
- <dependency>
- <groupId>${james.groupId}</groupId>
- <artifactId>james-server-jwt</artifactId>
- </dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>testing-base</artifactId>
@@ -58,14 +50,6 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
- <dependency>
- <groupId>jakarta.annotation</groupId>
- <artifactId>jakarta.annotation-api</artifactId>
- </dependency>
- <dependency>
- <groupId>jakarta.inject</groupId>
- <artifactId>jakarta.inject-api</artifactId>
- </dependency>
</dependencies>
</project>
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/AuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/AuthenticationProcessor.java
deleted file mode 100644
index 50edcbb24e..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/AuthenticationProcessor.java
+++ /dev/null
@@ -1,34 +0,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.
- *
- */
-
-package org.apache.james.managesieve.api;
-
-import org.apache.james.core.Username;
-
-public interface AuthenticationProcessor {
-
- String initialServerResponse(Session session);
-
- /**
- * @return Null if authentication failed, the authenticated username if authentication is successful
- */
- Username isAuthenticationSuccesfull(Session session, String suppliedClientData) throws SyntaxException, AuthenticationException;
-
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
index 9f1a058f19..606ac3c3ea 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
@@ -23,11 +23,13 @@ package org.apache.james.managesieve.api;
import java.util.Optional;
import org.apache.james.core.Username;
-import org.apache.james.jwt.OidcSASLConfiguration;
-import org.apache.james.managesieve.api.commands.Authenticate;
+import org.apache.james.protocols.api.sasl.SaslExchange;
public interface Session {
+ record ActiveSaslExchange(String mechanismName, SaslExchange exchange) {
+ }
+
enum State {
UNAUTHENTICATED,
AUTHENTICATION_IN_PROGRESS,
@@ -46,15 +48,18 @@ public interface Session {
void setState(State state);
- Authenticate.SupportedMechanism getChoosedAuthenticationMechanism();
+ Optional<ActiveSaslExchange> getActiveSaslExchange();
- void setChoosedAuthenticationMechanism(Authenticate.SupportedMechanism choosedAuthenticationMechanism);
+ void setActiveSaslExchange(ActiveSaslExchange activeSaslExchange);
+
+ Optional<ActiveSaslExchange> clearActiveSaslExchange();
void setSslEnabled(boolean sslEnabled);
boolean isSslEnabled();
- Optional<OidcSASLConfiguration> getOidcSASLConfiguration();
+ void setStartTlsSupported(boolean startTlsSupported);
+
+ boolean supportStartTLS();
- void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration);
}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/UnknownSaslMechanism.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/UnknownSaslMechanism.java
deleted file mode 100644
index ebef4a4c25..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/UnknownSaslMechanism.java
+++ /dev/null
@@ -1,28 +0,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.
- *
- */
-
-package org.apache.james.managesieve.api;
-
-public class UnknownSaslMechanism extends ManageSieveException {
-
- public UnknownSaslMechanism(String unknownMechanism) {
- super("Unknown SASL mechanism " + unknownMechanism);
- }
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
deleted file mode 100644
index 4e4820cd90..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
+++ /dev/null
@@ -1,48 +0,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.
- *
- */
-
-package org.apache.james.managesieve.api.commands;
-
-import org.apache.james.managesieve.api.Session;
-import org.apache.james.managesieve.api.UnknownSaslMechanism;
-
-
-/**
- * @see <a href=http://tools.ietf.org/html/rfc5804#section-2.1>RFC 5804 AUTHENTICATE Command</a>
- */
-public interface Authenticate {
-
- enum SupportedMechanism {
- PLAIN, XOAUTH2, OAUTHBEARER;
-
- public static SupportedMechanism retrieveMechanism(String serializedData) throws UnknownSaslMechanism {
- for (SupportedMechanism supportedMechanism : SupportedMechanism.values()) {
- if (supportedMechanism.toString().equalsIgnoreCase(serializedData)) {
- return supportedMechanism;
- }
- }
- throw new UnknownSaslMechanism(serializedData);
- }
- }
-
- String chooseMechanism(Session session, String mechanism);
-
- String authenticate(Session session, String suppliedData);
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
index 6016b9c192..7f5a6ed98e 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
@@ -26,6 +26,6 @@ package org.apache.james.managesieve.api.commands;
* @see <a href=http://tools.ietf.org/html/rfc5804#section-2>RFC 5804 Commands</a>
*/
public interface CoreCommands extends Capability, CheckScript, DeleteScript, GetScript, HaveSpace,
- ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, Authenticate, StartTLS {
+ ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, StartTLS {
}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/LineCommands.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/LineCommands.java
index 01cc9b3388..d2bc97eade 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/LineCommands.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/LineCommands.java
@@ -25,6 +25,6 @@ package org.apache.james.managesieve.api.commands;
*
* @see <a href=http://tools.ietf.org/html/rfc5804#section-1.8>RFC 5804 Transport</a>
*/
-public interface LineCommands extends Authenticate, Unauthenticate, Logout, Noop {
+public interface LineCommands extends Unauthenticate, Logout, Noop {
}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index 0224dac400..4b6186e910 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -27,21 +27,17 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
-import jakarta.inject.Inject;
-
import org.apache.commons.io.IOUtils;
-import org.apache.james.core.Username;
import org.apache.james.managesieve.api.AuthenticationException;
-import org.apache.james.managesieve.api.AuthenticationProcessor;
import org.apache.james.managesieve.api.AuthenticationRequiredException;
import org.apache.james.managesieve.api.ManageSieveException;
import org.apache.james.managesieve.api.Session;
import org.apache.james.managesieve.api.SessionTerminatedException;
import org.apache.james.managesieve.api.SieveParser;
import org.apache.james.managesieve.api.SyntaxException;
-import org.apache.james.managesieve.api.UnknownSaslMechanism;
import org.apache.james.managesieve.api.commands.CoreCommands;
import org.apache.james.managesieve.util.ParserUtils;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
import org.apache.james.sieverepository.api.ScriptContent;
import org.apache.james.sieverepository.api.ScriptName;
import org.apache.james.sieverepository.api.SieveRepository;
@@ -51,11 +47,11 @@ import org.apache.james.sieverepository.api.exception.QuotaExceededException;
import org.apache.james.sieverepository.api.exception.ScriptNotFoundException;
import org.apache.james.sieverepository.api.exception.SieveRepositoryException;
import org.apache.james.sieverepository.api.exception.StorageException;
-import org.apache.james.user.api.UsersRepository;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
public class CoreProcessor implements CoreCommands {
@@ -70,15 +66,13 @@ public class CoreProcessor implements CoreCommands {
private final SieveRepository sieveRepository;
private final SieveParser parser;
private final Map<Capabilities, String> capabilitiesBase;
- private final Map<SupportedMechanism, AuthenticationProcessor> authenticationProcessorMap;
+ private final ImmutableList<SaslMechanism> saslMechanisms;
- @Inject
- public CoreProcessor(SieveRepository repository, UsersRepository usersRepository, SieveParser parser) {
+ public CoreProcessor(SieveRepository repository, SieveParser parser, ImmutableList<SaslMechanism> saslMechanisms) {
this.sieveRepository = repository;
this.parser = parser;
this.capabilitiesBase = precomputedCapabilitiesBase(parser);
- this.authenticationProcessorMap = new HashMap<>();
- this.authenticationProcessorMap.put(SupportedMechanism.PLAIN, new PlainAuthenticationProcessor(usersRepository));
+ this.saslMechanisms = saslMechanisms;
}
@Override
@@ -96,17 +90,17 @@ public class CoreProcessor implements CoreCommands {
private Map<Capabilities, String> computeCapabilityMap(Session session) {
Map<Capabilities, String> capabilities = Maps.newHashMap(capabilitiesBase);
- if (!session.isSslEnabled()) {
+ if (!session.isSslEnabled() && session.supportStartTLS()) {
capabilities.put(Capabilities.STARTTLS, null);
}
if (session.isAuthenticated()) {
capabilities.put(Capabilities.OWNER, session.getUser().asString());
}
- session.getOidcSASLConfiguration().ifPresent(oidcConfiguration -> {
- this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.XOAUTH2, new OAUTHAuthenticationProcessor(oidcConfiguration));
- this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.OAUTHBEARER, new OAUTHAuthenticationProcessor(oidcConfiguration));
- });
- capabilities.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
+ String saslMechanisms = constructSaslSupportedAuthenticationMechanisms(session);
+ // RFC 5804 section 1.7 allows an empty SASL list only when STARTTLS is also advertised.
+ if (!saslMechanisms.isEmpty() || capabilities.containsKey(Capabilities.STARTTLS)) {
+ capabilities.put(Capabilities.SASL, saslMechanisms);
+ }
return capabilities;
}
@@ -211,60 +205,6 @@ public class CoreProcessor implements CoreCommands {
return "OK " + taggify(tag) + " \"DONE\"";
}
- @Override
- public String chooseMechanism(Session session, String mechanism) {
- try {
- if (Strings.isNullOrEmpty(mechanism)) {
- throw new SyntaxException("quoted SASL mechanism must be supplied");
- }
-
- SupportedMechanism supportedMechanism = SupportedMechanism.retrieveMechanism(mechanism);
- if (!this.authenticationProcessorMap.containsKey(supportedMechanism)) {
- throw new UnknownSaslMechanism("SASL mechanism disabled: " + mechanism);
- }
-
- session.setChoosedAuthenticationMechanism(supportedMechanism);
- session.setState(Session.State.AUTHENTICATION_IN_PROGRESS);
- AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(supportedMechanism);
- return authenticationProcessor.initialServerResponse(session);
- } catch (UnknownSaslMechanism e) {
- resetSession(session);
- return "NO \"" + e.getMessage() + "\"";
- } catch (SyntaxException e) {
- resetSession(session);
- return "NO \"ManageSieve syntax is incorrect: " + e.getMessage() + "\"";
- }
- }
-
- @Override
- public String authenticate(Session session, String suppliedData) {
- try {
- SupportedMechanism currentAuthenticationMechanism = session.getChoosedAuthenticationMechanism();
- AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(currentAuthenticationMechanism);
- if (Strings.isNullOrEmpty(suppliedData)) {
- throw new SyntaxException("authentication data must be supplied");
- }
- if (suppliedData.equals("*")) {
- throw new AuthenticationException("authentication aborted by client");
- }
- Username authenticatedUsername = authenticationProcessor.isAuthenticationSuccesfull(session, suppliedData);
- if (authenticatedUsername != null) {
- session.setUser(authenticatedUsername);
- session.setState(Session.State.AUTHENTICATED);
- return "OK";
- } else {
- resetSession(session);
- return "NO \"authentication failed\"";
- }
- } catch (AuthenticationException e) {
- resetSession(session);
- return "NO \"Authentication failed with: " + e.getMessage() + "\"";
- } catch (SyntaxException e) {
- resetSession(session);
- return "NO \"ManageSieve syntax is incorrect: " + e.getMessage() + "\"";
- }
- }
-
@Override
public String unauthenticate(Session session) {
if (session.isAuthenticated()) {
@@ -278,7 +218,6 @@ public class CoreProcessor implements CoreCommands {
private static void resetSession(Session session) {
session.setState(Session.State.UNAUTHENTICATED);
session.setUser(null);
- session.setChoosedAuthenticationMechanism(null);
}
@Override
@@ -354,11 +293,11 @@ public class CoreProcessor implements CoreCommands {
return capabilitiesBase;
}
- private String constructSaslSupportedAuthenticationMechanisms() {
- return Joiner.on(' ').join(this.authenticationProcessorMap
- .keySet()
+ private String constructSaslSupportedAuthenticationMechanisms(Session session) {
+ return Joiner.on(' ').join(this.saslMechanisms
.stream()
- .map(Enum::toString)
+ .filter(mechanism -> mechanism.isAvailableOnTransport(session.isSslEnabled()))
+ .map(SaslMechanism::name)
.iterator()
);
}
@@ -366,4 +305,4 @@ public class CoreProcessor implements CoreCommands {
private String sanitizeString(String message) {
return Joiner.on("\r\n").join(Splitter.on('\n').split(message));
}
-}
\ No newline at end of file
+}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
deleted file mode 100644
index ebdfe25c33..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
+++ /dev/null
@@ -1,75 +0,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.
- *
- */
-
-package org.apache.james.managesieve.core;
-
-import java.util.Optional;
-
-import org.apache.james.core.Username;
-import org.apache.james.jwt.OidcJwtTokenVerifier;
-import org.apache.james.jwt.OidcSASLConfiguration;
-import org.apache.james.managesieve.api.AuthenticationException;
-import org.apache.james.managesieve.api.AuthenticationProcessor;
-import org.apache.james.managesieve.api.Session;
-import org.apache.james.managesieve.api.SyntaxException;
-import org.apache.james.protocols.api.OIDCSASLParser;
-import org.apache.james.protocols.api.OIDCSASLParser.OIDCInitialResponse;
-
-public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
-
- private final OidcSASLConfiguration oidcConfiguration;
-
- public OAUTHAuthenticationProcessor(OidcSASLConfiguration oidcConfiguration) {
- this.oidcConfiguration = oidcConfiguration;
- }
-
- @Override
- public String initialServerResponse(Session session) {
- return "+ \"\"";
- }
-
- @Override
- public Username isAuthenticationSuccesfull(Session session, String suppliedClientData) throws SyntaxException, AuthenticationException {
- Optional<OIDCInitialResponse> oidcInitialResponseResult = OIDCSASLParser.parse(suppliedClientData);
- if (oidcInitialResponseResult.isEmpty()) {
- throw new SyntaxException("Could not parse the given JWT");
- }
- OIDCInitialResponse oidcInitialResponse = oidcInitialResponseResult.get();
-
- Optional<Username> authenticatedUserResult = Optional.empty();
- try {
- authenticatedUserResult = new OidcJwtTokenVerifier(this.oidcConfiguration).validateToken(oidcInitialResponse.getToken());
- } catch (Exception e) {
- throw new AuthenticationException("Could not validate the JWT");
- }
- if (authenticatedUserResult.isEmpty()) {
- throw new AuthenticationException("Could not validate the JWT");
- }
- Username authenticatedUser = authenticatedUserResult.get();
-
- // The user from the managesieve AUTHENTICATE command must match the username in the token.
- Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser());
- if (!authenticatedUser.equals(associatedUser)) {
- throw new AuthenticationException("Mismatch between user from command and JWT");
- }
-
- return authenticatedUser;
- }
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
deleted file mode 100644
index 1e9e659638..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
+++ /dev/null
@@ -1,94 +0,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.
- *
- */
-
-package org.apache.james.managesieve.core;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Iterator;
-
-import org.apache.james.core.Username;
-import org.apache.james.managesieve.api.AuthenticationException;
-import org.apache.james.managesieve.api.AuthenticationProcessor;
-import org.apache.james.managesieve.api.Session;
-import org.apache.james.managesieve.api.SyntaxException;
-import org.apache.james.user.api.UsersRepository;
-import org.apache.james.user.api.UsersRepositoryException;
-import org.apache.james.user.api.model.User;
-
-import com.google.common.base.Splitter;
-
-/**
- * See RFC-4616 : https://tools.ietf.org/html/rfc4616
- *
- * Only differences is that ManageSieve does not handle tags. See https://tools.ietf.org/html/rfc5804#section-2.1 for details
- */
-public class PlainAuthenticationProcessor implements AuthenticationProcessor {
-
- private final UsersRepository usersRepository;
-
- public PlainAuthenticationProcessor(UsersRepository usersRepository) {
- this.usersRepository = usersRepository;
- }
-
- @Override
- public String initialServerResponse(Session session) {
- return "+ \"\"";
- }
-
-
- @Override
- public Username isAuthenticationSuccesfull(Session session, String suppliedClientData) throws SyntaxException, AuthenticationException {
- try {
- byte[] decoded = Base64.getDecoder().decode(suppliedClientData.getBytes());
- String decodedString = new String(decoded, StandardCharsets.US_ASCII);
- return authenticateWithSeparator(session, decodedString, '\u0000');
- } catch (Exception e) {
- if (suppliedClientData.contains("\u0000")) {
- return authenticateWithSeparator(session, suppliedClientData, '\u0000');
- } else {
- return authenticateWithSeparator(session, suppliedClientData, ' ');
- }
- }
- }
-
- private Username authenticateWithSeparator(Session session, String suppliedClientData, char c) throws SyntaxException, AuthenticationException {
- Iterator<String> it = Splitter.on(c).omitEmptyStrings().split(suppliedClientData).iterator();
- if (!it.hasNext()) {
- throw new SyntaxException("You must supply a username for the authentication mechanism. Formal syntax: <NULL>username<NULL>password");
- }
- Username userName = Username.of(it.next());
- if (!it.hasNext()) {
- throw new SyntaxException("You must supply a password for the authentication mechanism. Formal syntax: <NULL>username<NULL>password");
- }
- String password = it.next();
- session.setUser(userName);
- try {
- User user = usersRepository.getUserByName(userName);
- if (user != null && user.verifyPassword(password)) {
- return user.getUserName();
- } else {
- throw new AuthenticationException("Verification of credentials failed");
- }
- } catch (UsersRepositoryException e) {
- throw new AuthenticationException(e.getMessage());
- }
- }
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
index c6d2cb16f6..5132191b5d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
@@ -22,8 +22,6 @@ package org.apache.james.managesieve.transcode;
import java.util.Iterator;
-import jakarta.inject.Inject;
-
import org.apache.james.managesieve.api.ArgumentException;
import org.apache.james.managesieve.api.Session;
import org.apache.james.managesieve.api.SessionTerminatedException;
@@ -42,7 +40,6 @@ public class ArgumentParser {
private final CoreCommands core;
private final boolean validatePutSize;
- @Inject
public ArgumentParser(CoreCommands core) {
this.core = core;
this.validatePutSize = true;
@@ -76,14 +73,6 @@ public class ArgumentParser {
core.logout();
}
- public String chooseMechanism(Session session, String mechanism) {
- return core.chooseMechanism(session, mechanism);
- }
-
- public String authenticate(Session session, String suppliedData) {
- return core.authenticate(session, suppliedData);
- }
-
public String deleteScript(Session session, String args) {
Iterator<String> argumentIterator = Splitter.on(' ').omitEmptyStrings().split(args).iterator();
if (!argumentIterator.hasNext()) {
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
index 391e9203b9..bfa2f1d608 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
@@ -20,16 +20,16 @@
package org.apache.james.managesieve.transcode;
-import jakarta.inject.Inject;
-
import org.apache.james.managesieve.api.ManageSieveException;
import org.apache.james.managesieve.api.Session;
import org.apache.james.managesieve.api.SessionTerminatedException;
-import org.apache.james.managesieve.util.ParserUtils;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
import org.apache.james.sieverepository.api.exception.SieveRepositoryException;
-public class ManageSieveProcessor {
+import com.google.common.collect.ImmutableList;
+public class ManageSieveProcessor {
public static final String AUTHENTICATE = "AUTHENTICATE";
public static final String CAPABILITY = "CAPABILITY";
public static final String CHECKSCRIPT = "CHECKSCRIPT";
@@ -47,80 +47,58 @@ public class ManageSieveProcessor {
public static final String UNAUTHENTICATE = "UNAUTHENTICATE";
private final ArgumentParser argumentParser;
+ private final ManageSieveSaslProcessor saslProcessor;
- @Inject
- public ManageSieveProcessor(ArgumentParser argumentParser) {
+ public ManageSieveProcessor(ArgumentParser argumentParser,
+ ImmutableList<SaslMechanism> saslMechanisms,
+ SaslAuthenticator saslAuthenticator) {
this.argumentParser = argumentParser;
+ this.saslProcessor = new ManageSieveSaslProcessor(saslMechanisms, saslAuthenticator);
}
public String handleRequest(Session session, String request) throws ManageSieveException, SieveRepositoryException {
- if (request.endsWith("\n")) {
- request = request.substring(0, request.length() - 1);
- }
- if (request.endsWith("\r")) {
- request = request.substring(0, request.length() - 1);
- }
-
+ String requestWithoutLineEnding = removeLineEnding(request);
if (session.getState() == Session.State.AUTHENTICATION_IN_PROGRESS) {
- return matchCommandWithImplementation(session, request.trim(), AUTHENTICATE) + "\r\n";
+ return saslProcessor.handleContinuation(session, requestWithoutLineEnding) + "\r\n";
}
- int firstWordEndIndex = request.indexOf(' ');
- String arguments = parseArguments(request, firstWordEndIndex);
- String command = parseCommand(request, firstWordEndIndex);
+ int firstWordEndIndex = requestWithoutLineEnding.indexOf(' ');
+ String arguments = parseArguments(requestWithoutLineEnding, firstWordEndIndex);
+ String command = parseCommand(requestWithoutLineEnding, firstWordEndIndex);
return matchCommandWithImplementation(session, arguments, command) + "\r\n";
}
+ public void close(Session session) {
+ saslProcessor.close(session);
+ }
+
+ private String removeLineEnding(String request) {
+ if (request.endsWith("\r\n")) {
+ return request.substring(0, request.length() - 2);
+ }
+ if (request.endsWith("\n") || request.endsWith("\r")) {
+ return request.substring(0, request.length() - 1);
+ }
+ return request;
+ }
+
private String parseCommand(String request, int firstWordEndIndex) {
- String command;
- if (request.contains(" ")) {
- command = request.substring(0, firstWordEndIndex);
- } else {
- command = request;
+ if (firstWordEndIndex >= 0) {
+ return request.substring(0, firstWordEndIndex);
}
- return command;
+ return request;
}
private String parseArguments(String request, int firstWordEndIndex) {
- if (request.contains(" ")) {
- return request.substring(firstWordEndIndex).trim();
- } else {
- return "";
+ if (firstWordEndIndex >= 0) {
+ return request.substring(firstWordEndIndex + 1).trim();
}
+ return "";
}
private String matchCommandWithImplementation(Session session, String arguments, String command) throws SessionTerminatedException {
if (command.equalsIgnoreCase(AUTHENTICATE)) {
- // The RFC forbids the AUTHENTICATE command if the session is already authenticated.
- if (session.isAuthenticated()) {
- return "NO \"already authenticated\"";
- }
-
- // If no authentication is in progress, the authentication mechanism needs to be chosen.
- if (session.getState() != Session.State.AUTHENTICATION_IN_PROGRESS) {
- String mechanism = ParserUtils.unquoteFirst(arguments);
- String result = argumentParser.chooseMechanism(session, mechanism);
- // If the authentication is not in progress, return the result (error) because choosing the mechanism has failed.
- if (session.getState() != Session.State.AUTHENTICATION_IN_PROGRESS) {
- return result;
- }
-
- // Skips the whole mechanism, the closing quote, and the space if present.
- // If the request is well-formatted, the arguments are now empty or contain the client's initial response.
- arguments = arguments.substring(arguments.indexOf(mechanism) + mechanism.length() + 1);
- if (arguments.startsWith(" ")) {
- arguments = arguments.substring(1);
- }
- // If there are is no initial client response left, return the result (initial server response).
- if (arguments.isEmpty()) {
- return result;
- }
- // Unquote the argument in this case because continuation is not used.
- arguments = ParserUtils.unquoteFirst(arguments);
- }
-
- // The authentication is in progress, the mechanism has been chosen, and the arguments contain an initial client response.
- return argumentParser.authenticate(session, arguments);
+ return saslProcessor.startAuthentication(session, arguments);
} else if (command.equalsIgnoreCase(CAPABILITY)) {
return argumentParser.capability(session, arguments);
} else if (command.equalsIgnoreCase(CHECKSCRIPT)) {
@@ -154,5 +132,4 @@ public class ManageSieveProcessor {
public String getAdvertisedCapabilities(Session session) {
return argumentParser.capability(session, "") + "\r\n";
}
-
}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveSaslProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveSaslProcessor.java
new file mode 100644
index 0000000000..287b1a0c24
--- /dev/null
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveSaslProcessor.java
@@ -0,0 +1,248 @@
+/****************************************************************
+ * 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.managesieve.transcode;
+
+import java.util.Optional;
+
+import org.apache.james.managesieve.api.Session;
+import org.apache.james.managesieve.api.Session.ActiveSaslExchange;
+import org.apache.james.managesieve.api.SyntaxException;
+import org.apache.james.managesieve.sasl.ManageSieveSaslCodec;
+import org.apache.james.protocols.api.sasl.SaslAuthenticator;
+import org.apache.james.protocols.api.sasl.SaslExchange;
+import org.apache.james.protocols.api.sasl.SaslFailure;
+import org.apache.james.protocols.api.sasl.SaslMechanism;
+import org.apache.james.protocols.api.sasl.SaslStep;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableList;
+
+class ManageSieveSaslProcessor {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ManageSieveSaslProcessor.class);
+
+ private final ImmutableList<SaslMechanism> saslMechanisms;
+ private final SaslAuthenticator saslAuthenticator;
+
+ ManageSieveSaslProcessor(ImmutableList<SaslMechanism> saslMechanisms, SaslAuthenticator saslAuthenticator) {
+ this.saslMechanisms = saslMechanisms;
+ this.saslAuthenticator = saslAuthenticator;
+ }
+
+ String startAuthentication(Session session, String arguments) {
+ if (session.isAuthenticated()) {
+ return "NO \"already authenticated\"";
+ }
+ try {
+ ManageSieveSaslCodec.InitialRequest request = ManageSieveSaslCodec.parseInitialRequest(arguments);
+ Optional<SaslMechanism> mechanism = findMechanism(request.mechanismName());
+ if (mechanism.isEmpty()) {
+ return "NO \"Unknown SASL mechanism\"";
+ }
+ SaslMechanism selectedMechanism = mechanism.get();
+ if (!selectedMechanism.isAvailableOnTransport(session.isSslEnabled())) {
+ if (!session.isSslEnabled() && selectedMechanism.isAvailableOnTransport(true)) {
+ return "NO (ENCRYPT-NEEDED) \"Authentication requires an encrypted channel\"";
+ }
+ return "NO \"Unknown SASL mechanism\"";
+ }
+ return startExchange(session, request, selectedMechanism);
+ } catch (NotEnoughDataException e) {
+ // RFC 5804 section 2.1 permits an initial SASL response as a literal spanning multiple network frames.
+ throw e;
+ } catch (SyntaxException e) {
+ resetAuthentication(session);
+ return syntaxFailure(e);
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ return unexpectedAuthenticationFailure(e);
+ }
+ }
+
+ String handleContinuation(Session session, String suppliedData) {
+ ActiveSaslExchange active = session.getActiveSaslExchange()
+ .orElseThrow(() -> new IllegalStateException("Missing active SASL exchange"));
+ if (ManageSieveSaslCodec.isAbort(suppliedData)) {
+ try {
+ return "NO \"Authentication failed with: authentication aborted by client\"";
+ } finally {
+ closeAndReset(session);
+ }
+ }
+ try {
+ byte[] clientResponse = parseClientResponse(session, active, suppliedData);
+ return handleSaslStep(session, onResponse(session, active.exchange(), clientResponse));
+ } catch (NotEnoughDataException e) {
+ // Keep both the channel cumulation and the active exchange until the complete literal arrives.
+ throw e;
+ } catch (SyntaxException e) {
+ closeAndReset(session);
+ return syntaxFailure(e);
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ return unexpectedAuthenticationFailure(e);
+ }
+ }
+
+ void close(Session session) {
+ try {
+ closeActiveSaslExchange(session);
+ } finally {
+ resetAuthentication(session);
+ }
+ }
+
+ private String startExchange(Session session, ManageSieveSaslCodec.InitialRequest request, SaslMechanism mechanism) {
+ ActiveSaslExchange active = new ActiveSaslExchange(request.mechanismName(), mechanism.start(request.saslInitialRequest(), saslAuthenticator));
+ registerActiveSaslExchange(session, active);
+ return handleSaslStep(session, firstStep(session, active.exchange()));
+ }
+
+ private void registerActiveSaslExchange(Session session, ActiveSaslExchange active) {
+ if (session.getActiveSaslExchange().isPresent()) {
+ active.exchange().close();
+ throw new IllegalStateException("A SASL exchange is already active");
+ }
+ try {
+ session.setActiveSaslExchange(active);
+ } catch (RuntimeException e) {
+ active.exchange().close();
+ throw e;
+ }
+ }
+
+ private SaslStep firstStep(Session session, SaslExchange exchange) {
+ try {
+ return exchange.firstStep();
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ throw e;
+ }
+ }
+
+ private byte[] parseClientResponse(Session session, ActiveSaslExchange active, String suppliedData) throws SyntaxException {
+ try {
+ return ManageSieveSaslCodec.parseClientResponse(active.mechanismName(), suppliedData);
+ } catch (NotEnoughDataException | SyntaxException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ throw e;
+ }
+ }
+
+ private SaslStep onResponse(Session session, SaslExchange exchange, byte[] clientResponse) {
+ try {
+ return exchange.onResponse(clientResponse);
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ throw e;
+ }
+ }
+
+ private String handleSaslStep(Session session, SaslStep step) {
+ return switch (step) {
+ case SaslStep.Challenge challenge -> handleChallenge(session, challenge);
+ case SaslStep.Success success -> handleSuccess(session, success);
+ case SaslStep.Failure failure -> handleFailure(session, failure);
+ };
+ }
+
+ private String handleChallenge(Session session, SaslStep.Challenge challenge) {
+ try {
+ String response = ManageSieveSaslCodec.challenge(challenge);
+ session.setState(Session.State.AUTHENTICATION_IN_PROGRESS);
+ return response;
+ } catch (RuntimeException e) {
+ closeAndReset(session);
+ throw e;
+ }
+ }
+
+ private String handleSuccess(Session session, SaslStep.Success success) {
+ try {
+ String response = ManageSieveSaslCodec.success(success);
+ session.setUser(success.identity().authorizationId());
+ session.setState(Session.State.AUTHENTICATED);
+ return response;
+ } catch (RuntimeException e) {
+ resetAuthentication(session);
+ throw e;
+ } finally {
+ closeActiveSaslExchange(session);
+ }
+ }
+
+ private String handleFailure(Session session, SaslStep.Failure failure) {
+ try {
+ return authenticationFailure(failure.failure());
+ } finally {
+ closeAndReset(session);
+ }
+ }
+
+ private Optional<SaslMechanism> findMechanism(String mechanismName) {
+ return saslMechanisms.stream()
+ .filter(mechanism -> mechanism.name().equalsIgnoreCase(mechanismName))
+ .findFirst();
+ }
+
+ private String authenticationFailure(SaslFailure failure) {
+ if (failure.type() == SaslFailure.Type.SERVER_ERROR) {
+ failure.cause().ifPresentOrElse(
+ cause -> LOGGER.error("ManageSieve SASL authentication failed", cause),
+ () -> LOGGER.error("ManageSieve SASL authentication failed: {}", failure.reason()));
+ }
+ if (failure.type() == SaslFailure.Type.MALFORMED) {
+ return "NO \"ManageSieve syntax is incorrect: authentication data is malformed\"";
+ }
+ if (failure.type() == SaslFailure.Type.INVALID_CREDENTIALS) {
+ return "NO \"Authentication failed with: Verification of credentials failed\"";
+ }
+ return "NO \"authentication failed\"";
+ }
+
+ private String unexpectedAuthenticationFailure(RuntimeException exception) {
+ LOGGER.error("ManageSieve SASL authentication failed", exception);
+ return "NO \"authentication failed\"";
+ }
+
+ private String syntaxFailure(SyntaxException e) {
+ return "NO \"ManageSieve syntax is incorrect: " + e.getMessage() + "\"";
+ }
+
+ private void resetAuthentication(Session session) {
+ session.setState(Session.State.UNAUTHENTICATED);
+ session.setUser(null);
+ }
+
+ private void closeAndReset(Session session) {
+ try {
+ closeActiveSaslExchange(session);
+ } finally {
+ resetAuthentication(session);
+ }
+ }
+
+ private void closeActiveSaslExchange(Session session) {
+ session.clearActiveSaslExchange()
+ .ifPresent(active -> active.exchange().close());
+ }
+}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
index 204a39e688..16e741a78d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
@@ -23,17 +23,15 @@ package org.apache.james.managesieve.util;
import java.util.Optional;
import org.apache.james.core.Username;
-import org.apache.james.jwt.OidcSASLConfiguration;
import org.apache.james.managesieve.api.Session;
-import org.apache.james.managesieve.api.commands.Authenticate;
public class SettableSession implements Session {
private Username user;
private State state;
- private Authenticate.SupportedMechanism choosedAuthenticationMechanism;
+ private Optional<ActiveSaslExchange> activeSaslExchange = Optional.empty();
private boolean sslEnabled;
- private Optional<OidcSASLConfiguration> oidcSASLConfiguration = Optional.empty();
+ private boolean startTlsSupported;
public SettableSession() {
this.state = State.UNAUTHENTICATED;
@@ -66,13 +64,20 @@ public class SettableSession implements Session {
}
@Override
- public Authenticate.SupportedMechanism getChoosedAuthenticationMechanism() {
- return choosedAuthenticationMechanism;
+ public Optional<ActiveSaslExchange> getActiveSaslExchange() {
+ return activeSaslExchange;
}
@Override
- public void setChoosedAuthenticationMechanism(Authenticate.SupportedMechanism choosedAuthenticationMechanism) {
- this.choosedAuthenticationMechanism = choosedAuthenticationMechanism;
+ public void setActiveSaslExchange(ActiveSaslExchange activeSaslExchange) {
+ this.activeSaslExchange = Optional.of(activeSaslExchange);
+ }
+
+ @Override
+ public Optional<ActiveSaslExchange> clearActiveSaslExchange() {
+ Optional<ActiveSaslExchange> previousExchange = activeSaslExchange;
+ this.activeSaslExchange = Optional.empty();
+ return previousExchange;
}
@Override
@@ -86,12 +91,13 @@ public class SettableSession implements Session {
}
@Override
- public Optional<OidcSASLConfiguration> getOidcSASLConfiguration() {
- return this.oidcSASLConfiguration;
+ public void setStartTlsSupported(boolean startTlsSupported) {
+ this.startTlsSupported = startTlsSupported;
}
@Override
- public void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration) {
- this.oidcSASLConfiguration = configuration;
+ public boolean supportStartTLS() {
+ return startTlsSupported;
}
+
}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org