diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index 94cd215614..46b80db0d4 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -41,6 +41,7 @@ * @param Value type. * @author Will Glozer * @author Mark Paluch + * @author Tugdual Grall */ @SuppressWarnings("unchecked") public abstract class AbstractRedisAsyncCommands implements RedisHashAsyncCommands, RedisKeyAsyncCommands, diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 54dae38b2f..c341d96c94 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -47,6 +47,7 @@ * @param Value type. * @author Mark Paluch * @author Nikolai Perevozchikov + * @author Tugdual Grall * @since 4.0 */ public abstract class AbstractRedisReactiveCommands implements RedisHashReactiveCommands, diff --git a/src/main/java/io/lettuce/core/ConnectionState.java b/src/main/java/io/lettuce/core/ConnectionState.java index 25bfc06d41..90df2aa46c 100644 --- a/src/main/java/io/lettuce/core/ConnectionState.java +++ b/src/main/java/io/lettuce/core/ConnectionState.java @@ -15,6 +15,8 @@ */ package io.lettuce.core; +import java.util.List; + import io.lettuce.core.protocol.ProtocolVersion; /** @@ -95,7 +97,27 @@ void setHandshakeResponse(HandshakeResponse handshakeResponse) { this.handshakeResponse = handshakeResponse; } - void setUsername(String username) { + /** + * Sets username/password state based on the argument count from an {@code AUTH} command. + * + * @param args + */ + protected void setUserNamePassword(List args) { + + if (args.isEmpty()) { + return; + } + + if (args.size() > 1) { + setUsername(new String(args.get(0))); + setPassword(args.get(1)); + } else { + setUsername(null); + setPassword(args.get(0)); + } + } + + protected void setUsername(String username) { this.username = username; } diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 9055ccbcef..4c2753db29 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -35,6 +35,7 @@ * @param * @author Mark Paluch * @author Zhang Jessey + * @author Tugdual Grall */ @SuppressWarnings({ "unchecked", "varargs" }) class RedisCommandBuilder extends BaseRedisCommandBuilder { diff --git a/src/main/java/io/lettuce/core/RedisHandshake.java b/src/main/java/io/lettuce/core/RedisHandshake.java index 1b96a4eac3..b449355de7 100644 --- a/src/main/java/io/lettuce/core/RedisHandshake.java +++ b/src/main/java/io/lettuce/core/RedisHandshake.java @@ -34,6 +34,7 @@ * connection state restoration. This class is part of the internal API. * * @author Mark Paluch + * @author Tugdual Grall * @since 6.0 */ class RedisHandshake implements ConnectionInitializer { @@ -151,8 +152,9 @@ private CompletableFuture initializeResp3(Channel channel) { * @return */ private CompletableFuture initiateHandshakeResp2(Channel channel) { + if (connectionState.hasUsername()) { - return dispatch(channel, this.commandBuilder.auth(connectionState.getUsername() ,connectionState.getPassword())); + return dispatch(channel, this.commandBuilder.auth(connectionState.getUsername(), connectionState.getPassword())); } else if (connectionState.hasPassword()) { return dispatch(channel, this.commandBuilder.auth(connectionState.getPassword())); } else if (this.pingOnConnect) { diff --git a/src/main/java/io/lettuce/core/RedisURI.java b/src/main/java/io/lettuce/core/RedisURI.java index 240244e48d..842206ffb6 100644 --- a/src/main/java/io/lettuce/core/RedisURI.java +++ b/src/main/java/io/lettuce/core/RedisURI.java @@ -64,19 +64,23 @@ * *

URI syntax

* - * Redis Standalone
redis{@code ://}[password@]host [{@code :} - * port][{@code /}database][{@code ?} [timeout=timeout[d|h|m|s|ms|us|ns]] [ - * &database=database] [&clientName=clientName]]
+ * Redis Standalone
redis{@code ://}[[username{@code :}]password@]host + * [{@code :} port][{@code /}database][{@code ?} + * [timeout=timeout[d|h|m|s|ms|us|ns]] [ &database=database] [&clientName=clientName]] + *
* - * Redis Standalone (SSL)
rediss{@code ://}[password@]host [{@code :} + * Redis Standalone (SSL)
+ * rediss{@code ://}[[username{@code :}]password@]host [{@code :} * port][{@code /}database][{@code ?} [timeout=timeout[d|h|m|s|ms|us|ns]] [ * &database=database] [&clientName=clientName]]
* - * Redis Standalone (Unix Domain Sockets)
redis-socket{@code ://} [password@]path[ + * Redis Standalone (Unix Domain Sockets)
redis-socket{@code ://} + * [[username{@code :}]password@]path[ * {@code ?}[timeout=timeout[d|h|m|s|ms|us|ns]][&database=database] * [&clientName=clientName]]
* - * Redis Sentinel
redis-sentinel{@code ://}[password@]host1 [{@code :} + * Redis Sentinel
+ * redis-sentinel{@code ://}[[username{@code :}]password@]host1 [{@code :} * port1][, host2 [{@code :}port2]][, hostN [{@code :}portN]][{@code /} * database][{@code ?} [timeout=timeout[d|h|m|s|ms|us|ns]] [ * &sentinelMasterId=sentinelMasterId] [&database=database] [&clientName=clientName]] @@ -86,6 +90,9 @@ * Note: When using Redis Sentinel, the password from the URI applies to the data nodes only. Sentinel authentication must be * configured for each {@link #getSentinels() sentinel node}. *

+ *

+ * Note:Usernames are supported as of Redis 6. + *

* *

* Schemes diff --git a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java index 20a3dbcc2c..5f79e8d421 100644 --- a/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java +++ b/src/main/java/io/lettuce/core/StatefulRedisConnectionImpl.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; @@ -157,16 +158,14 @@ protected RedisCommand preProcessCommand(RedisCommand comm local = attachOnComplete(local, status -> { if ("OK".equals(status)) { - char[] password = CommandArgsAccessor.getFirstCharArray(command.getArgs()); + List args = CommandArgsAccessor.getCharArrayArguments(command.getArgs()); - if (password != null) { - state.setPassword(password); + if (!args.isEmpty()) { + state.setUserNamePassword(args); } else { - String stringPassword = CommandArgsAccessor.getFirstString(command.getArgs()); - if (stringPassword != null) { - state.setPassword(stringPassword.toCharArray()); - } + List strings = CommandArgsAccessor.getStringArguments(command.getArgs()); + state.setUserNamePassword(strings.stream().map(String::toCharArray).collect(Collectors.toList())); } } }); diff --git a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java index a2abc70caf..0ff16b257f 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisAsyncCommands.java @@ -42,11 +42,12 @@ public interface RedisAsyncCommands extends BaseRedisAsyncCommands, RedisFuture auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ RedisFuture auth(String username, CharSequence password); diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java index 1f31717b1a..0c999add37 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisReactiveCommands.java @@ -42,11 +42,12 @@ public interface RedisReactiveCommands extends BaseRedisReactiveCommands auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ Mono auth(String username, CharSequence password); diff --git a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java index 25885a8e17..5c5113efd7 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisCommands.java @@ -33,21 +33,22 @@ public interface RedisCommands extends BaseRedisCommands, RedisClust RedisStreamCommands, RedisStringCommands, RedisTransactionalCommands { /** - * Authenticate to the server with username and password. + * Authenticate to the server. * - * @param username the username * @param password the password * @return String simple-string-reply */ - String auth(String username, CharSequence password); + String auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * + * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ - String auth(CharSequence password); + String auth(String username, CharSequence password); /** * Change the selected database for the current Commands. diff --git a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java index 8e4c60a3d8..dac67f5b9f 100644 --- a/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java +++ b/src/main/java/io/lettuce/core/cluster/StatefulRedisClusterConnectionImpl.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import java.util.stream.Collectors; import io.lettuce.core.*; import io.lettuce.core.api.StatefulRedisConnection; @@ -195,16 +196,15 @@ private RedisCommand preProcessCommand(RedisCommand comman if (local.getType().name().equals(AUTH.name())) { local = attachOnComplete(local, status -> { if (status.equals("OK")) { - char[] password = CommandArgsAccessor.getFirstCharArray(command.getArgs()); + List args = CommandArgsAccessor.getCharArrayArguments(command.getArgs()); - if (password != null) { - this.connectionState.setPassword(password); + if (!args.isEmpty()) { + this.connectionState.setUserNamePassword(args); } else { - String stringPassword = CommandArgsAccessor.getFirstString(command.getArgs()); - if (stringPassword != null) { - this.connectionState.setPassword(stringPassword.toCharArray()); - } + List stringArgs = CommandArgsAccessor.getStringArguments(command.getArgs()); + this.connectionState + .setUserNamePassword(stringArgs.stream().map(String::toCharArray).collect(Collectors.toList())); } } }); @@ -264,8 +264,8 @@ ConnectionState getConnectionState() { static class ClusterConnectionState extends ConnectionState { @Override - protected void setPassword(char[] password) { - super.setPassword(password); + protected void setUserNamePassword(List args) { + super.setUserNamePassword(args); } @Override diff --git a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java index 49b4d39a55..9a99156f27 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/RedisClusterAsyncCommands.java @@ -54,11 +54,12 @@ public interface RedisClusterAsyncCommands extends BaseRedisAsyncCommands< RedisFuture auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ RedisFuture auth(String username, CharSequence password); diff --git a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java index db7bf8dd39..8ba83de011 100644 --- a/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/reactive/RedisClusterReactiveCommands.java @@ -55,11 +55,12 @@ public interface RedisClusterReactiveCommands extends BaseRedisReactiveCom Mono auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ Mono auth(String username, CharSequence password); diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java index 2994fd390a..68fe1209ec 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/RedisClusterCommands.java @@ -51,11 +51,12 @@ public interface RedisClusterCommands extends BaseRedisCommands, Red String auth(CharSequence password); /** - * Authenticate to the server. + * Authenticate to the server with username and password. Requires Redis 6 or newer. * * @param username the username * @param password the password * @return String simple-string-reply + * @since 6.0 */ String auth(String username, CharSequence password); diff --git a/src/main/java/io/lettuce/core/protocol/CommandArgsAccessor.java b/src/main/java/io/lettuce/core/protocol/CommandArgsAccessor.java index f510fc1e5a..f278eb6406 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandArgsAccessor.java +++ b/src/main/java/io/lettuce/core/protocol/CommandArgsAccessor.java @@ -16,6 +16,8 @@ package io.lettuce.core.protocol; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; import io.lettuce.core.protocol.CommandArgs.CharArrayArgument; import io.lettuce.core.protocol.CommandArgs.SingularArgument; @@ -69,7 +71,7 @@ public static String getFirstString(CommandArgs commandArgs) { } /** - * Get the first {@link char}-array argument. + * Get the first {@code char[]}-array argument. * * @param commandArgs must not be null. * @return the first {@link String} argument or {@literal null}. @@ -87,6 +89,52 @@ public static char[] getFirstCharArray(CommandArgs commandArgs) { return null; } + /** + * Get the all {@link String} arguments. + * + * @param commandArgs must not be null. + * @return the first {@link String} argument or {@literal null}. + * @since 6.0 + */ + public static List getStringArguments(CommandArgs commandArgs) { + + List args = new ArrayList<>(); + + for (SingularArgument singularArgument : commandArgs.singularArguments) { + + if (singularArgument instanceof StringArgument) { + args.add(((StringArgument) singularArgument).val); + } + } + + return args; + } + + /** + * Get the all {@code char[]} arguments. + * + * @param commandArgs must not be null. + * @return the first {@link String} argument or {@literal null}. + * @since 6.0 + */ + public static List getCharArrayArguments(CommandArgs commandArgs) { + + List args = new ArrayList<>(); + + for (SingularArgument singularArgument : commandArgs.singularArguments) { + + if (singularArgument instanceof CharArrayArgument) { + args.add(((CharArrayArgument) singularArgument).val); + } + + if (singularArgument instanceof StringArgument) { + args.add(((StringArgument) singularArgument).val.toCharArray()); + } + } + + return args; + } + /** * Get the first {@link Long integer} argument. * diff --git a/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java b/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java index 45bf5fadae..17ba804ba4 100644 --- a/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ClientOptionsIntegrationTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import io.lettuce.test.condition.EnabledOnCommand; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import io.lettuce.core.api.StatefulRedisConnection; @@ -131,6 +132,7 @@ void testHitRequestQueueLimitReconnectWithAuthCommand() { } @Test + @EnabledOnCommand("ACL") void testHitRequestQueueLimitReconnectWithAuthUsernamePasswordCommand() { WithPassword.run(client, () -> { diff --git a/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java b/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java index 27ae88be39..c56bb5d93b 100644 --- a/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ConnectionCommandIntegrationTests.java @@ -25,7 +25,6 @@ import javax.inject.Inject; -import io.lettuce.core.protocol.RedisCommand; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,10 +34,12 @@ import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.core.protocol.ProtocolVersion; import io.lettuce.test.*; +import io.lettuce.test.condition.EnabledOnCommand; /** * @author Will Glozer * @author Mark Paluch + * @author Tugdual Grall */ @ExtendWith(LettuceExtension.class) class ConnectionCommandIntegrationTests extends TestSupport { @@ -64,25 +65,44 @@ void auth() { client.setOptions( ClientOptions.builder().pingBeforeActivateConnection(false).protocolVersion(ProtocolVersion.RESP2).build()); RedisCommands connection = client.connect().sync(); - try { - connection.ping(); - fail("Server doesn't require authentication"); - } catch (RedisException e) { - assertThat(e.getMessage()).isEqualTo("NOAUTH Authentication required."); - assertThat(connection.auth(passwd)).isEqualTo("OK"); - assertThat(connection.set(key, value)).isEqualTo("OK"); - - // Aut with the same user & password (default) - assertThat(connection.auth(username, passwd)).isEqualTo("OK"); - assertThat(connection.set(key, value)).isEqualTo("OK"); - - // Switch to another user - assertThat(connection.auth(sampleUsername, samplePasswd)).isEqualTo("OK"); - assertThat(connection.set("cached:demo", value)).isEqualTo("OK"); - assertThatThrownBy(() -> connection.get(key)).isInstanceOf(RedisCommandExecutionException.class); - assertThat(connection.del("cached:demo")).isEqualTo(1); - } + assertThatThrownBy(connection::ping).isInstanceOf(RedisException.class) + .hasMessageContaining("NOAUTH Authentication required"); + + assertThat(connection.auth(passwd)).isEqualTo("OK"); + assertThat(connection.set(key, value)).isEqualTo("OK"); + + RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(2).withPassword(passwd).build(); + RedisCommands authConnection = client.connect(redisURI).sync(); + authConnection.ping(); + authConnection.getStatefulConnection().close(); + }); + } + + @Test + @EnabledOnCommand("ACL") + void authWithUsername() { + + WithPassword.run(client, () -> { + client.setOptions( + ClientOptions.builder().pingBeforeActivateConnection(false).protocolVersion(ProtocolVersion.RESP2).build()); + RedisCommands connection = client.connect().sync(); + + assertThatThrownBy(connection::ping).isInstanceOf(RedisException.class) + .hasMessageContaining("NOAUTH Authentication required"); + + assertThat(connection.auth(passwd)).isEqualTo("OK"); + assertThat(connection.set(key, value)).isEqualTo("OK"); + + // Aut with the same user & password (default) + assertThat(connection.auth(username, passwd)).isEqualTo("OK"); + assertThat(connection.set(key, value)).isEqualTo("OK"); + + // Switch to another user + assertThat(connection.auth(aclUsername, aclPasswd)).isEqualTo("OK"); + assertThat(connection.set("cached:demo", value)).isEqualTo("OK"); + assertThatThrownBy(() -> connection.get(key)).isInstanceOf(RedisCommandExecutionException.class); + assertThat(connection.del("cached:demo")).isEqualTo(1); RedisURI redisURI = RedisURI.Builder.redis(host, port).withDatabase(2).withPassword(passwd).build(); RedisCommands authConnection = client.connect(redisURI).sync(); @@ -92,12 +112,15 @@ void auth() { } @Test + @EnabledOnCommand("ACL") void resp2HandShakeWithUsernamePassword() { - RedisURI redisURI = RedisURI.Builder.redis(host, port).withAuthentication(username,passwd).build(); + + RedisURI redisURI = RedisURI.Builder.redis(host, port).withAuthentication(username, passwd).build(); RedisClient clientResp2 = RedisClient.create(redisURI); clientResp2.setOptions( ClientOptions.builder().pingBeforeActivateConnection(false).protocolVersion(ProtocolVersion.RESP2).build()); RedisCommands connTestResp2 = null; + try { connTestResp2 = clientResp2.connect().sync(); assertThat(redis.ping()).isEqualTo("PONG"); @@ -131,17 +154,36 @@ void select() { @Test void authNull() { assertThatThrownBy(() -> redis.auth(null)).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> redis.auth(null,"x")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> redis.auth(null, "x")).isInstanceOf(IllegalArgumentException.class); } @Test void authEmpty() { assertThatThrownBy(() -> redis.auth("")).isInstanceOf(IllegalArgumentException.class); - assertThatThrownBy(() -> redis.auth("","x")).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> redis.auth("", "x")).isInstanceOf(IllegalArgumentException.class); } @Test void authReconnect() { + WithPassword.run(client, () -> { + + client.setOptions( + ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false).build()); + RedisCommands connection = client.connect().sync(); + assertThat(connection.auth(passwd)).isEqualTo("OK"); + assertThat(connection.set(key, value)).isEqualTo("OK"); + connection.quit(); + + Delay.delay(Duration.ofMillis(100)); + assertThat(connection.get(key)).isEqualTo(value); + + connection.getStatefulConnection().close(); + }); + } + + @Test + @EnabledOnCommand("ACL") + void authReconnectRedis6() { WithPassword.run(client, () -> { client.setOptions( @@ -201,22 +243,30 @@ void authInvalidPassword() { } @Test + @EnabledOnCommand("ACL") void authInvalidUsernamePassword() { WithPassword.run(client, () -> { client.setOptions( ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false).build()); RedisCommands connection = client.connect().sync(); + assertThat(connection.auth(username, passwd)).isEqualTo("OK"); - assertThatThrownBy(() -> connection.auth( username,"invalid")).hasMessage("WRONGPASS invalid username-password pair"); - assertThat(connection.auth(sampleUsername, samplePasswd)).isEqualTo("OK"); - assertThatThrownBy(() -> connection.auth( sampleUsername,"invalid")).hasMessage("WRONGPASS invalid username-password pair"); - connection.getStatefulConnection().close(); + assertThatThrownBy(() -> connection.auth(username, "invalid")) + .hasMessage("WRONGPASS invalid username-password pair"); + + assertThat(connection.auth(aclUsername, aclPasswd)).isEqualTo("OK"); + + assertThatThrownBy(() -> connection.auth(aclUsername, "invalid")) + .hasMessage("WRONGPASS invalid username-password pair"); + + connection.getStatefulConnection().close(); }); } @Test + @EnabledOnCommand("ACL") void authInvalidDefaultPasswordNoACL() { RedisAsyncCommands async = client.connect().async(); // When the database is not secured the AUTH default invalid command returns OK diff --git a/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java b/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java index 5266151620..b7fae489c0 100644 --- a/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java +++ b/src/test/java/io/lettuce/core/ReactiveConnectionIntegrationTests.java @@ -28,7 +28,6 @@ import javax.enterprise.inject.New; import javax.inject.Inject; -import io.lettuce.test.WithPassword; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @@ -45,10 +44,13 @@ import io.lettuce.test.Delay; import io.lettuce.test.LettuceExtension; import io.lettuce.test.Wait; +import io.lettuce.test.WithPassword; +import io.lettuce.test.condition.EnabledOnCommand; /** * @author Mark Paluch * @author Nikolai Perevozchikov + * @author Tugdual Grall */ @ExtendWith(LettuceExtension.class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -203,17 +205,31 @@ void transactional(RedisClient client) throws Exception { @Test void auth() { - StepVerifier.create(reactive.auth("error")).expectError().verify(); - StepVerifier.create(reactive.auth(username, "error")).expectNext("OK").verifyComplete(); + WithPassword.enableAuthentication(this.connection.sync()); + + try { + StepVerifier.create(reactive.auth("error")).expectError().verify(); + } finally { + WithPassword.disableAuthentication(this.connection.sync()); + } + } + + @Test + @EnabledOnCommand("ACL") + void authWithUsername() { - WithPassword.enableAuthentication( this.connection.sync()); + try { - StepVerifier.create(reactive.auth(username, "error")).expectError().verify(); + StepVerifier.create(reactive.auth(username, "error")).expectNext("OK").verifyComplete(); - StepVerifier.create(reactive.auth(sampleUsername, samplePasswd)).expectNext("OK").verifyComplete(); - StepVerifier.create(reactive.auth(sampleUsername, "error")).expectError().verify(); + WithPassword.enableAuthentication(this.connection.sync()); - WithPassword.disableAuthentication( this.connection.sync()); + StepVerifier.create(reactive.auth(username, "error")).expectError().verify(); + StepVerifier.create(reactive.auth(aclUsername, aclPasswd)).expectNext("OK").verifyComplete(); + StepVerifier.create(reactive.auth(aclUsername, "error")).expectError().verify(); + } finally { + WithPassword.disableAuthentication(this.connection.sync()); + } } @Test @@ -239,14 +255,11 @@ void subscribeWithDisconnectedClient(RedisClient client) { connection.async().quit(); Wait.untilTrue(() -> !connection.isOpen()).waitOrTimeout(); - StepVerifier - .create(connection.reactive().ping()) - .consumeErrorWith( - throwable -> { - assertThat(throwable).isInstanceOf(RedisException.class).hasMessageContaining( - "not connected. Commands are rejected"); + StepVerifier.create(connection.reactive().ping()).consumeErrorWith(throwable -> { + assertThat(throwable).isInstanceOf(RedisException.class) + .hasMessageContaining("not connected. Commands are rejected"); - }).verify(); + }).verify(); connection.close(); } diff --git a/src/test/java/io/lettuce/core/TestSupport.java b/src/test/java/io/lettuce/core/TestSupport.java index 410ffcda8a..61cdb7b1c5 100644 --- a/src/test/java/io/lettuce/core/TestSupport.java +++ b/src/test/java/io/lettuce/core/TestSupport.java @@ -24,6 +24,7 @@ /** * @author Mark Paluch + * @author Tugdual Grall */ public abstract class TestSupport { @@ -32,8 +33,8 @@ public abstract class TestSupport { public static final String username = TestSettings.username(); public static final String passwd = TestSettings.password(); - public static final String sampleUsername = TestSettings.sampleUsername(); - public static final String samplePasswd = TestSettings.samplePassword(); + public static final String aclUsername = TestSettings.aclUsername(); + public static final String aclPasswd = TestSettings.aclPassword(); public static final String key = "key"; public static final String value = "value"; diff --git a/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java b/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java index 54f6442838..bc83302d3e 100644 --- a/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java +++ b/src/test/java/io/lettuce/core/pubsub/PubSubCommandTest.java @@ -42,12 +42,14 @@ import io.lettuce.test.TestFutures; import io.lettuce.test.Wait; import io.lettuce.test.WithPassword; +import io.lettuce.test.condition.EnabledOnCommand; import io.lettuce.test.resource.FastShutdown; import io.lettuce.test.resource.TestClientResources; /** * @author Will Glozer * @author Mark Paluch + * @author Tugdual Grall */ class PubSubCommandTest extends AbstractRedisClientTest implements RedisPubSubListener { @@ -98,6 +100,7 @@ void auth() { } @Test + @EnabledOnCommand("ACL") void authWithUsername() { WithPassword.run(client, () -> { @@ -105,7 +108,7 @@ void authWithUsername() { ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false).build()); RedisPubSubAsyncCommands connection = client.connectPubSub().async(); connection.getStatefulConnection().addListener(PubSubCommandTest.this); - connection.auth(username,passwd); + connection.auth(username, passwd); connection.subscribe(channel); assertThat(channels.take()).isEqualTo(channel); @@ -123,13 +126,42 @@ void authWithReconnect() { RedisPubSubAsyncCommands connection = client.connectPubSub().async(); connection.getStatefulConnection().addListener(PubSubCommandTest.this); connection.auth(passwd); + connection.clientSetname("authWithReconnect"); - connection.subscribe(channel); + connection.subscribe(channel).get(); assertThat(channels.take()).isEqualTo(channel); - redis.auth(username, passwd); long id = findNamedClient("authWithReconnect"); + redis.auth(passwd); + redis.clientKill(KillArgs.Builder.id(id)); + + Delay.delay(Duration.ofMillis(100)); + Wait.untilTrue(connection::isOpen).waitOrTimeout(); + + assertThat(channels.take()).isEqualTo(channel); + }); + } + + @Test + @EnabledOnCommand("ACL") + void authWithUsernameAndReconnect() { + + WithPassword.run(client, () -> { + + client.setOptions( + ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false).build()); + + RedisPubSubAsyncCommands connection = client.connectPubSub().async(); + connection.getStatefulConnection().addListener(PubSubCommandTest.this); + connection.auth(username, passwd); + connection.clientSetname("authWithReconnect"); + connection.subscribe(channel).get(); + + assertThat(channels.take()).isEqualTo(channel); + + long id = findNamedClient("authWithReconnect"); + redis.auth(username, passwd); redis.clientKill(KillArgs.Builder.id(id)); Delay.delay(Duration.ofMillis(100)); diff --git a/src/test/java/io/lettuce/examples/ConnectToRedis.java b/src/test/java/io/lettuce/examples/ConnectToRedis.java index 8cd6e73f73..744fcaf466 100644 --- a/src/test/java/io/lettuce/examples/ConnectToRedis.java +++ b/src/test/java/io/lettuce/examples/ConnectToRedis.java @@ -20,6 +20,7 @@ /** * @author Mark Paluch + * @author Tugdual Grall */ public class ConnectToRedis { diff --git a/src/test/java/io/lettuce/examples/ConnectToRedisCluster.java b/src/test/java/io/lettuce/examples/ConnectToRedisCluster.java index 59e7ce93a2..41cec7d01d 100644 --- a/src/test/java/io/lettuce/examples/ConnectToRedisCluster.java +++ b/src/test/java/io/lettuce/examples/ConnectToRedisCluster.java @@ -20,6 +20,7 @@ /** * @author Mark Paluch + * @author Tugdual Grall */ public class ConnectToRedisCluster { diff --git a/src/test/java/io/lettuce/test/WithPassword.java b/src/test/java/io/lettuce/test/WithPassword.java index 94e70bac11..d51b986735 100644 --- a/src/test/java/io/lettuce/test/WithPassword.java +++ b/src/test/java/io/lettuce/test/WithPassword.java @@ -17,18 +17,11 @@ import java.lang.reflect.UndeclaredThrowableException; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; -import io.lettuce.core.codec.StringCodec; -import io.lettuce.core.output.StatusOutput; -import io.lettuce.core.protocol.AsyncCommand; import io.lettuce.core.protocol.Command; -import io.lettuce.core.protocol.CommandType; -import io.lettuce.core.protocol.RedisCommand; import io.lettuce.test.condition.RedisConditions; import io.lettuce.test.settings.TestSettings; @@ -36,12 +29,10 @@ * Utility to run a {@link ThrowingCallable callback function} while Redis is configured with a password. * * @author Mark Paluch + * @author Tugdual Grall */ public class WithPassword { - private boolean hasACLCommand = false; - - /** * Run a {@link ThrowingCallable callback function} while Redis is configured with a password. * @@ -83,7 +74,7 @@ public static void enableAuthentication(RedisCommands commands) // If ACL is supported let's create a test user if (conditions.hasCommand("ACL")) { Command> command = CliParser.parse( - "ACL SETUSER "+ TestSettings.sampleUsername() +" on >"+ TestSettings.samplePassword() +" ~cached:* +@all"); + "ACL SETUSER " + TestSettings.aclUsername() + " on >" + TestSettings.aclPassword() + " ~cached:* +@all"); commands.dispatch(command.getType(), command.getOutput(), command.getArgs()); } } @@ -95,12 +86,13 @@ public static void enableAuthentication(RedisCommands commands) */ public static void disableAuthentication(RedisCommands commands) { - RedisConditions conditions = RedisConditions.of(commands); commands.auth(TestSettings.password()); // reauthenticate as default user before disabling it + + RedisConditions conditions = RedisConditions.of(commands); commands.configSet("requirepass", ""); if (conditions.hasCommand("ACL")) { - Command> command = CliParser.parse("ACL DELUSER "+ TestSettings.sampleUsername()); + Command> command = CliParser.parse("ACL DELUSER " + TestSettings.aclUsername()); commands.dispatch(command.getType(), command.getOutput(), command.getArgs()); command = CliParser.parse("acl setuser default nopass"); diff --git a/src/test/java/io/lettuce/test/settings/TestSettings.java b/src/test/java/io/lettuce/test/settings/TestSettings.java index 33f81123a9..e240a97528 100644 --- a/src/test/java/io/lettuce/test/settings/TestSettings.java +++ b/src/test/java/io/lettuce/test/settings/TestSettings.java @@ -23,10 +23,11 @@ * This class provides settings used while testing. You can override these using system properties. * * @author Mark Paluch + * @author Tugdual Grall */ public class TestSettings { - private TestSettings() { + private TestSettings() { } /** @@ -75,7 +76,6 @@ public static String hostAddr() { } } - /** * * @return default username of your redis instance. @@ -96,19 +96,19 @@ public static String password() { /** * * @return password of a second user your redis instance. Defaults to {@literal lettuceTest}. Can be overridden with - * {@code --Dsample.username=SampleUsername} + * {@code -Dacl.username=SampleUsername} */ - public static String sampleUsername() { - return System.getProperty("sample.username", "lettuceTest"); + public static String aclUsername() { + return System.getProperty("acl.username", "lettuceTest"); } /** * - * @return password of a second user of your redis instance. Defaults to {@literal lettuceTestPasswd}. Can be overridden with - * {@code -Dsample.password=SamplePassword} + * @return password of a second user of your redis instance. Defaults to {@literal lettuceTestPasswd}. Can be overridden + * with {@code -Dacl.password=SamplePassword} */ - public static String samplePassword() { - return System.getProperty("sample.password", "lettuceTestPasswd"); + public static String aclPassword() { + return System.getProperty("acl.password", "lettuceTestPasswd"); } /** @@ -116,7 +116,7 @@ public static String samplePassword() { * @return port of your redis instance. Defaults to {@literal 6479}. Can be overriden with {@code -Dport=1234} */ public static int port() { - return Integer.valueOf(System.getProperty("port", "6479")); + return Integer.parseInt(System.getProperty("port", "6479")); } /** @@ -124,7 +124,7 @@ public static int port() { * @return sslport of your redis instance. Defaults to {@literal 6443}. Can be overriden with {@code -Dsslport=1234} */ public static int sslPort() { - return Integer.valueOf(System.getProperty("sslport", "6443")); + return Integer.parseInt(System.getProperty("sslport", "6443")); } /**