From c21b359b5eb7b8a9c3b05fa2a4bd2c90469c4a8d Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Fri, 6 Sep 2024 09:25:26 +0200 Subject: [PATCH] Let the client run fine when the scram dependency is not available. Motivation: The scram dependency is optional and the module descriptor declares it as static, however in practice when the scram dependency is not available the client cannot be used because the ScramAuthentication is unavailable due to a classloading error. The client should be resilient to this common case and continue to work when scram is not available. Changes: Introduce an indirection level, the scram interactions are now handled by a ScramSession and the ScramAuthentication is a factory for sessions. When the scram dependency is not available the ScramAuthentication instance is null, the InitCommandCodec can check this field and throw an exception that will close the connection (as per the protocol requirements). Result: The client runs fine without the scram dependency on the class/module path. --- vertx-pg-client/pom.xml | 15 +++++ vertx-pg-client/src/main/asciidoc/index.adoc | 2 + .../impl/auth/scram/ScramAuthentication.java | 31 ++++++++++ .../impl/auth/scram/ScramSession.java | 52 +++++++++++++++++ .../scram/ScramSessionImpl.java} | 27 +++++---- .../pgclient/impl/codec/InitCommandCodec.java | 19 +++++-- .../vertx/pgclient/it/MissingScramTest.java | 57 +++++++++++++++++++ 7 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java create mode 100644 vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSession.java rename vertx-pg-client/src/main/java/io/vertx/pgclient/impl/{util/ScramAuthentication.java => auth/scram/ScramSessionImpl.java} (96%) create mode 100644 vertx-pg-client/src/test/java/io/vertx/pgclient/it/MissingScramTest.java diff --git a/vertx-pg-client/pom.xml b/vertx-pg-client/pom.xml index 44c659b627..18f6828544 100644 --- a/vertx-pg-client/pom.xml +++ b/vertx-pg-client/pom.xml @@ -161,6 +161,21 @@ + + missing-scram-test + + integration-test + + integration-test + + + io/vertx/pgclient/it/MissingScramTest.java + + + com.ongres.scram:scram-client + + + diff --git a/vertx-pg-client/src/main/asciidoc/index.adoc b/vertx-pg-client/src/main/asciidoc/index.adoc index 0dddca21b4..2ade01339a 100644 --- a/vertx-pg-client/src/main/asciidoc/index.adoc +++ b/vertx-pg-client/src/main/asciidoc/index.adoc @@ -254,6 +254,8 @@ dependencies { } ---- +When the database requires a scram authentication and the scram client jar is not on the class/module path, the connection will be closed by the client. + include::queries.adoc[leveloffset=1] == Returning clauses diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java new file mode 100644 index 0000000000..658977c4f5 --- /dev/null +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramAuthentication.java @@ -0,0 +1,31 @@ +package io.vertx.pgclient.impl.auth.scram; + +import com.ongres.scram.client.ScramClient; +import io.vertx.core.internal.logging.Logger; +import io.vertx.core.internal.logging.LoggerFactory; + +public class ScramAuthentication { + + private static final Logger logger = LoggerFactory.getLogger(ScramAuthentication.class); + + public static ScramAuthentication INSTANCE; + + static { + ScramAuthentication instance; + try { + ScramClient.MechanismsBuildStage builder = ScramClient.builder(); + logger.debug("Scram authentication is available " + builder); + instance = new ScramAuthentication(); + } catch (Throwable notFound) { + instance = null; + } + INSTANCE = instance; + } + + private ScramAuthentication() { + } + + public ScramSession session(String username, char[] password) { + return new ScramSessionImpl(username, password); + } +} diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSession.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSession.java new file mode 100644 index 0000000000..1cc05c5ec2 --- /dev/null +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSession.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2017 Julien Viet + * + * Licensed 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 io.vertx.pgclient.impl.auth.scram; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.vertx.pgclient.impl.codec.ScramClientInitialMessage; + +public interface ScramSession { + + /* + * The client selects one of the supported mechanisms from the list, + * and sends a SASLInitialResponse message to the server. + * The message includes the name of the selected mechanism, and + * an optional Initial Client Response, if the selected mechanism uses that. + */ + ScramClientInitialMessage createInitialSaslMessage(ByteBuf in, ChannelHandlerContext ctx); + + /* + * One or more server-challenge and client-response message will follow. + * Each server-challenge is sent in an AuthenticationSASLContinue message, + * followed by a response from client in an SASLResponse message. + * The particulars of the messages are mechanism specific. + */ + String receiveServerFirstMessage(ByteBuf in); + + /* + * Finally, when the authentication exchange is completed successfully, + * the server sends an AuthenticationSASLFinal message, followed immediately by an AuthenticationOk message. + * The AuthenticationSASLFinal contains additional server-to-client data, + * whose content is particular to the selected authentication mechanism. + * If the authentication mechanism doesn't use additional data that's sent at completion, + * the AuthenticationSASLFinal message is not sent + */ + void checkServerFinalMessage(ByteBuf in); + +} diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/ScramAuthentication.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java similarity index 96% rename from vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/ScramAuthentication.java rename to vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java index b1d8d69689..66fb7e797f 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/util/ScramAuthentication.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/auth/scram/ScramSessionImpl.java @@ -15,17 +15,7 @@ * */ -package io.vertx.pgclient.impl.util; - -import java.nio.charset.StandardCharsets; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.List; - -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLSession; +package io.vertx.pgclient.impl.auth.scram; import com.ongres.scram.client.ScramClient; import com.ongres.scram.common.StringPreparation; @@ -33,19 +23,28 @@ import com.ongres.scram.common.exception.ScramParseException; import com.ongres.scram.common.exception.ScramServerErrorException; import com.ongres.scram.common.util.TlsServerEndpoint; - import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslHandler; import io.vertx.pgclient.impl.codec.ScramClientInitialMessage; +import io.vertx.pgclient.impl.util.Util; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import java.nio.charset.StandardCharsets; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; -public class ScramAuthentication { +public class ScramSessionImpl implements ScramSession { private final String username; private final char[] password; private ScramClient scramClient; - public ScramAuthentication(String username, char[] password) { + public ScramSessionImpl(String username, char[] password) { this.username = username; this.password = password; } diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitCommandCodec.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitCommandCodec.java index 2fa0332cb7..a92505d1cb 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitCommandCodec.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/InitCommandCodec.java @@ -20,9 +20,11 @@ import java.nio.charset.StandardCharsets; import io.netty.buffer.ByteBuf; +import io.vertx.core.VertxException; import io.vertx.pgclient.impl.PgDatabaseMetadata; import io.vertx.pgclient.impl.PgSocketConnection; -import io.vertx.pgclient.impl.util.ScramAuthentication; +import io.vertx.pgclient.impl.auth.scram.ScramAuthentication; +import io.vertx.pgclient.impl.auth.scram.ScramSession; import io.vertx.sqlclient.internal.Connection; import io.vertx.sqlclient.internal.command.CommandResponse; import io.vertx.sqlclient.internal.command.InitCommand; @@ -31,7 +33,7 @@ class InitCommandCodec extends PgCommandCodec { private PgEncoder encoder; private String encoding; - private ScramAuthentication scramAuthentication; + private ScramSession scramSession; InitCommandCodec(InitCommand cmd) { super(cmd); @@ -57,21 +59,26 @@ public void handleAuthenticationClearTextPassword() { @Override void handleAuthenticationSasl(ByteBuf in) { - scramAuthentication = new ScramAuthentication(cmd.username(), cmd.password().toCharArray()); + ScramAuthentication scramAuth = ScramAuthentication.INSTANCE; + if (scramAuth == null) { + // This will close the connection + throw new VertxException("Scram authentication not supported, missing com.ongres.scram:scram-client on the class/module path"); + } + scramSession = scramAuth.session(cmd.username(), cmd.password().toCharArray()); encoder.writeScramClientInitialMessage( - scramAuthentication.createInitialSaslMessage(in, encoder.channelHandlerContext())); + scramSession.createInitialSaslMessage(in, encoder.channelHandlerContext())); encoder.flush(); } @Override void handleAuthenticationSaslContinue(ByteBuf in) { - encoder.writeScramClientFinalMessage(new ScramClientFinalMessage(scramAuthentication.receiveServerFirstMessage(in))); + encoder.writeScramClientFinalMessage(new ScramClientFinalMessage(scramSession.receiveServerFirstMessage(in))); encoder.flush(); } @Override void handleAuthenticationSaslFinal(ByteBuf in) { - scramAuthentication.checkServerFinalMessage(in); + scramSession.checkServerFinalMessage(in); } @Override diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/it/MissingScramTest.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/it/MissingScramTest.java new file mode 100644 index 0000000000..f938a07163 --- /dev/null +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/it/MissingScramTest.java @@ -0,0 +1,57 @@ +package io.vertx.pgclient.it; + +import io.vertx.core.Vertx; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgConnection; +import io.vertx.pgclient.junit.ContainerPgRule; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assume.assumeTrue; + +@RunWith(VertxUnitRunner.class) +public class MissingScramTest { + + @ClassRule + public static ContainerPgRule rule = new ContainerPgRule(); + + private Vertx vertx; + + private PgConnectOptions options; + + @Before + public void setup() throws Exception { + vertx = Vertx.vertx(); + options = rule.options(); + } + + private PgConnectOptions options() { + return new PgConnectOptions(options); + } + + @Test + public void testSaslConnectionFails(TestContext ctx) throws InterruptedException { + assumeTrue(ContainerPgRule.isAtLeastPg10()); + Async async = ctx.async(); + PgConnectOptions options = new PgConnectOptions(options()); + options.setUser("saslscram"); + options.setPassword("saslscrampwd"); + + PgConnection.connect(vertx, options).onComplete( + ctx.asyncAssertFailure(ar -> { + async.complete(); + }) + ); + } + + @After + public void teardown(TestContext ctx) { + vertx.close().onComplete(ctx.asyncAssertSuccess()); + } +}