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 <[email protected]> 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: [email protected] For additional commands, e-mail: [email protected]
