diff --git a/Makefile b/Makefile index 07f7c0ce92..e23a1519fc 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -REDIS_VERSION:=5.0.5 +REDIS_VERSION:=6.0.7 SPRING_PROFILE?=ci SHELL=/bin/bash -euo pipefail @@ -36,6 +36,24 @@ work/redis-%.conf: echo save \"\" >> $@ echo slaveof 127.0.0.1 6379 >> $@ +# Handled separately because it's a node with authentication. User: spring, password: data. Default password: foobared +work/redis-6382.conf: + @mkdir -p $(@D) + + echo port 6382 >> $@ + echo daemonize yes >> $@ + echo protected-mode no >> $@ + echo bind 0.0.0.0 >> $@ + echo notify-keyspace-events Ex >> $@ + echo pidfile $(shell pwd)/work/redis-6382.pid >> $@ + echo logfile $(shell pwd)/work/redis-6382.log >> $@ + echo unixsocket $(shell pwd)/work/redis-6382.sock >> $@ + echo unixsocketperm 755 >> $@ + echo "requirepass foobared" >> $@ + echo "user default on #1b58ee375b42e41f0e48ef2ff27d10a5b1f6924a9acdcdba7cae868e7adce6bf ~* +@all" >> $@ + echo "user spring on #3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7 +@all" >> $@ + echo save \"\" >> $@ + # Handled separately because it's the master and all others are slaves work/redis-6379.conf: @mkdir -p $(@D) @@ -54,9 +72,9 @@ work/redis-6379.conf: work/redis-%.pid: work/redis-%.conf work/redis/bin/redis-server work/redis/bin/redis-server $< -redis-start: work/redis-6379.pid work/redis-6380.pid work/redis-6381.pid +redis-start: work/redis-6379.pid work/redis-6380.pid work/redis-6381.pid work/redis-6382.pid -redis-stop: stop-6379 stop-6380 stop-6381 +redis-stop: stop-6379 stop-6380 stop-6381 stop-6382 ########## # Sentinel @@ -75,12 +93,29 @@ work/sentinel-%.conf: echo save \"\" >> $@ echo sentinel monitor mymaster 127.0.0.1 6379 2 >> $@ +# Password-protected Sentinel +work/sentinel-26382.conf: + @mkdir -p $(@D) + + echo port 26382 >> $@ + echo daemonize yes >> $@ + echo protected-mode no >> $@ + echo bind 0.0.0.0 >> $@ + echo pidfile $(shell pwd)/work/sentinel-26382.pid >> $@ + echo logfile $(shell pwd)/work/sentinel-26382.log >> $@ + echo save \"\" >> $@ + echo "requirepass foobared" >> $@ + echo "user default on #1b58ee375b42e41f0e48ef2ff27d10a5b1f6924a9acdcdba7cae868e7adce6bf ~* +@all" >> $@ + echo "user spring on #3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7 +@all" >> $@ + echo sentinel monitor mymaster 127.0.0.1 6382 2 >> $@ + echo sentinel auth-pass mymaster foobared >> $@ + work/sentinel-%.pid: work/sentinel-%.conf work/redis-6379.pid work/redis/bin/redis-server work/redis/bin/redis-server $< --sentinel -sentinel-start: work/sentinel-26379.pid work/sentinel-26380.pid work/sentinel-26381.pid +sentinel-start: work/sentinel-26379.pid work/sentinel-26380.pid work/sentinel-26381.pid work/sentinel-26382.pid -sentinel-stop: stop-26379 stop-26380 stop-26381 +sentinel-stop: stop-26379 stop-26380 stop-26381 stop-26382 ######### @@ -150,6 +185,12 @@ start: redis-start sentinel-start cluster-init stop-%: work/redis/bin/redis-cli -work/redis/bin/redis-cli -p $* shutdown +stop-6382: work/redis/bin/redis-cli + -work/redis/bin/redis-cli -a foobared -p 6382 shutdown + +stop-26382: work/redis/bin/redis-cli + -work/redis/bin/redis-cli -a foobared -p 26382 shutdown + stop: redis-stop sentinel-stop cluster-stop test: diff --git a/pom.xml b/pom.xml index 1403fde28e..6f2ee98641 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 2.4.0-SNAPSHOT + 2.4.0-DATAREDIS-1046-SNAPSHOT Spring Data Redis @@ -22,7 +22,7 @@ 1.9.2 1.4.12 2.7.0 - 5.3.3.RELEASE + 6.0.0.RC1 3.3.0 1.01 4.1.51.Final diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 5ae24df584..414264c332 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,9 +7,10 @@ This section briefly covers items that are new and noteworthy in the latest rele == New in Spring Data Redis 2.4 * `RedisCache` now exposes `CacheStatistics`. +* ACL authentication support for Redis Standalone, Redis Cluster and Master/Replica. +* Password support for Redis Sentinel using Jedis. [[new-in-2.3.0]] - == New in Spring Data Redis 2.3 * Template API Method Refinements for `Duration` and `Instant`. diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc index 2ea34a7925..5a719c387b 100644 --- a/src/main/asciidoc/reference/redis.adoc +++ b/src/main/asciidoc/reference/redis.adoc @@ -324,11 +324,6 @@ public RedisConnectionFactory lettuceConnectionFactory() { Sometimes, direct interaction with one of the Sentinels is required. Using `RedisConnectionFactory.getSentinelConnection()` or `RedisConnection.getSentinelCommands()` gives you access to the first active Sentinel configured. -[NOTE] -==== -Sentinel authentication is only available using https://lettuce.io/[Lettuce]. -==== - [[redis:template]] == Working with Objects through RedisTemplate diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java index 236ff1c2a6..7947da1f81 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.springframework.core.env.MapPropertySource; @@ -49,6 +50,7 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon private Set clusterNodes; private @Nullable Integer maxRedirects; + private Optional username = Optional.empty(); private RedisPassword password = RedisPassword.none(); /** @@ -182,6 +184,24 @@ private void appendClusterNodes(Set hostAndPorts) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String) + */ + @Override + public void setUsername(String username) { + this.username = Optional.of(username); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername() + */ + @Override + public Optional getUsername() { + return this.username; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword() diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java index 523d32af00..e860cf8819 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.IntSupplier; import java.util.function.Supplier; @@ -51,11 +52,11 @@ default Integer getDatabaseOrElse(Supplier other) { /** * Get the configured {@link RedisPassword} if the current {@link RedisConfiguration} is - * {@link #isPasswordAware(RedisConfiguration) password aware} or evaluate and return the value of the given + * {@link #isAuthenticationAware(RedisConfiguration) password aware} or evaluate and return the value of the given * {@link Supplier}. * * @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not - * {@link #isPasswordAware(RedisConfiguration) password aware}. + * {@link #isAuthenticationAware(RedisConfiguration) password aware}. * @return never {@literal null}. * @throws IllegalArgumentException if {@code other} is {@literal null}. */ @@ -67,8 +68,8 @@ default RedisPassword getPasswordOrElse(Supplier other) { * @param configuration can be {@literal null}. * @return {@code true} if given {@link RedisConfiguration} is instance of {@link WithPassword}. */ - static boolean isPasswordAware(@Nullable RedisConfiguration configuration) { - return configuration instanceof WithPassword; + static boolean isAuthenticationAware(@Nullable RedisConfiguration configuration) { + return configuration instanceof WithAuthentication; } /** @@ -136,14 +137,28 @@ static Integer getDatabaseOrElse(@Nullable RedisConfiguration configuration, Sup /** * @param configuration can be {@literal null}. * @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not - * {@link #isPasswordAware(RedisConfiguration) password aware}. + * {@link #isAuthenticationAware(RedisConfiguration) password aware}. + * @return never {@literal null}. + * @throws IllegalArgumentException if {@code other} is {@literal null}. + */ + static Optional getUsernameOrElse(@Nullable RedisConfiguration configuration, + Supplier> other) { + + Assert.notNull(other, "Other must not be null!"); + return isAuthenticationAware(configuration) ? ((WithAuthentication) configuration).getUsername() : other.get(); + } + + /** + * @param configuration can be {@literal null}. + * @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not + * {@link #isAuthenticationAware(RedisConfiguration) password aware}. * @return never {@literal null}. * @throws IllegalArgumentException if {@code other} is {@literal null}. */ static RedisPassword getPasswordOrElse(@Nullable RedisConfiguration configuration, Supplier other) { Assert.notNull(other, "Other must not be null!"); - return isPasswordAware(configuration) ? ((WithPassword) configuration).getPassword() : other.get(); + return isAuthenticationAware(configuration) ? ((WithAuthentication) configuration).getPassword() : other.get(); } /** @@ -178,9 +193,17 @@ static String getHostOrElse(@Nullable RedisConfiguration configuration, Supplier * {@link RedisConfiguration} part suitable for configurations that may use authentication when connecting. * * @author Christoph Strobl - * @since 2.1 + * @author Mark Paluch + * @since 2.4 */ - interface WithPassword { + interface WithAuthentication { + + /** + * Create and set a username with the given {@link String}. Requires Redis 6 or newer. + * + * @param username the username. + */ + void setUsername(String username); /** * Create and set a {@link RedisPassword} for given {@link String}. @@ -207,6 +230,13 @@ default void setPassword(@Nullable char[] password) { */ void setPassword(RedisPassword password); + /** + * Get the username to use when connecting. + * + * @return {@link Optional#empty()} if none set. + */ + Optional getUsername(); + /** * Get the RedisPassword to use when connecting. * @@ -215,6 +245,16 @@ default void setPassword(@Nullable char[] password) { RedisPassword getPassword(); } + /** + * {@link RedisConfiguration} part suitable for configurations that may use authentication when connecting. + * + * @author Christoph Strobl + * @since 2.1 + */ + interface WithPassword extends WithAuthentication { + + } + /** * {@link RedisConfiguration} part suitable for configurations that use a specific database. * @@ -339,7 +379,17 @@ default void setMaster(final String name) { Set getSentinels(); /** - * Get the {@link RedisPassword} used when authenticating with a Redis Server.. + * Get the username used when authenticating with a Redis Server. + * + * @return never {@literal null}. + * @since 2.4 + */ + default Optional getDataNodeUsername() { + return getUsername(); + } + + /** + * Get the {@link RedisPassword} used when authenticating with a Redis Server. * * @return never {@literal null}. * @since 2.2.2 diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java index 406e7bb728..8313b7a764 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -21,6 +21,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import org.springframework.core.env.MapPropertySource; @@ -50,6 +51,7 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC private Set sentinels; private int database; + private Optional dataNodeUsername = Optional.empty(); private RedisPassword dataNodePassword = RedisPassword.none(); private RedisPassword sentinelPassword = RedisPassword.none(); @@ -229,6 +231,24 @@ public void setDatabase(int index) { this.database = index; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String) + */ + @Override + public void setUsername(String username) { + this.dataNodeUsername = Optional.of(username); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername() + */ + @Override + public Optional getUsername() { + return this.dataNodeUsername; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword() diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java index c2b4336245..93118a5724 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSocketConfiguration.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.connection; +import java.util.Optional; + import org.springframework.data.redis.connection.RedisConfiguration.DomainSocketConfiguration; import org.springframework.util.Assert; @@ -32,6 +34,7 @@ public class RedisSocketConfiguration implements RedisConfiguration, DomainSocke private String socket = DEFAULT_SOCKET; private int database; + private Optional username = Optional.empty(); private RedisPassword password = RedisPassword.none(); /** @@ -92,6 +95,24 @@ public void setDatabase(int index) { this.database = index; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String) + */ + @Override + public void setUsername(String username) { + this.username = Optional.of(username); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername() + */ + @Override + public Optional getUsername() { + return this.username; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword() diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java index 12d2a96a0c..058b32af12 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStandaloneConfiguration.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.connection; +import java.util.Optional; + import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex; import org.springframework.data.redis.connection.RedisConfiguration.WithHostAndPort; import org.springframework.data.redis.connection.RedisConfiguration.WithPassword; @@ -37,6 +39,7 @@ public class RedisStandaloneConfiguration private String hostName = DEFAULT_HOST; private int port = DEFAULT_PORT; private int database; + private Optional username = Optional.empty(); private RedisPassword password = RedisPassword.none(); /** @@ -125,6 +128,24 @@ public void setDatabase(int index) { this.database = index; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String) + */ + @Override + public void setUsername(String username) { + this.username = Optional.of(username); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername() + */ + @Override + public Optional getUsername() { + return this.username; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword() diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java index c7a1ad2931..d31f5939e6 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStaticMasterReplicaConfiguration.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.springframework.data.redis.connection.RedisConfiguration.StaticMasterReplicaConfiguration; import org.springframework.util.Assert; @@ -40,6 +41,7 @@ public class RedisStaticMasterReplicaConfiguration implements RedisConfiguration private List nodes = new ArrayList<>(); private int database; + private Optional username = Optional.empty(); private RedisPassword password = RedisPassword.none(); /** @@ -130,6 +132,24 @@ public void setDatabase(int index) { this.nodes.forEach(it -> it.setDatabase(database)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String) + */ + @Override + public void setUsername(String username) { + this.username = Optional.of(username); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername() + */ + @Override + public Optional getUsername() { + return this.username; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword() diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index d65f8ca096..8cb3fbb6c1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -328,6 +328,7 @@ public void afterPropertiesSet() { clientConfiguration.getHostnameVerifier().orElse(null)); getRedisPassword().map(String::new).ifPresent(shardInfo::setPassword); + getRedisUsername().ifPresent(shardInfo::setUser); int readTimeout = getReadTimeout(); @@ -369,9 +370,13 @@ private Pool createPool() { */ protected Pool createRedisSentinelPool(RedisSentinelConfiguration config) { - GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(); + GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(); + String sentinelUser = null; + String sentinelPassword = config.getSentinelPassword().toOptional().map(String::new).orElse(null); + return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()), - poolConfig, getConnectTimeout(), getReadTimeout(), getPassword(), getDatabase(), getClientName()); + poolConfig, getConnectTimeout(), getReadTimeout(), getUsername(), getPassword(), getDatabase(), + getClientName(), getConnectTimeout(), getReadTimeout(), sentinelUser, sentinelPassword, getClientName()); } /** @@ -383,7 +388,7 @@ protected Pool createRedisSentinelPool(RedisSentinelConfiguration config) protected Pool createRedisPool() { return new JedisPool(getPoolConfig(), getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), - getPassword(), getDatabase(), getClientName(), isUseSsl(), + getUsername(), getPassword(), getDatabase(), getClientName(), isUseSsl(), clientConfiguration.getSslSocketFactory().orElse(null), // clientConfiguration.getSslParameters().orElse(null), // clientConfiguration.getHostnameVerifier().orElse(null)); @@ -414,7 +419,7 @@ protected ClusterTopologyProvider createTopologyProvider(JedisCluster cluster) { * @return the actual {@link JedisCluster}. * @since 1.7 */ - protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, GenericObjectPoolConfig poolConfig) { + protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, GenericObjectPoolConfig poolConfig) { Assert.notNull(clusterConfig, "Cluster configuration must not be null!"); @@ -425,7 +430,7 @@ protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, Ge int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects() : 5; - return new JedisCluster(hostAndPort, getConnectTimeout(), getReadTimeout(), redirects, getPassword(), + return new JedisCluster(hostAndPort, getConnectTimeout(), getReadTimeout(), redirects, getUsername(), getPassword(), getClientName(), poolConfig, isUseSsl(), clientConfiguration.getSslSocketFactory().orElse(null), clientConfiguration.getSslParameters().orElse(null), clientConfiguration.getHostnameVerifier().orElse(null), null); @@ -544,6 +549,16 @@ public void setUseSsl(boolean useSsl) { getMutableConfiguration().setUseSsl(useSsl); } + /** + * Returns the username used for authenticating with the Redis server. + * + * @return username for authentication. + */ + @Nullable + private String getUsername() { + return getRedisUsername().orElse(null); + } + /** * Returns the password used for authenticating with the Redis server. * @@ -554,6 +569,10 @@ public String getPassword() { return getRedisPassword().map(String::new).orElse(null); } + private Optional getRedisUsername() { + return RedisConfiguration.getUsernameOrElse(this.configuration, standaloneConfig::getUsername); + } + private RedisPassword getRedisPassword() { return RedisConfiguration.getPasswordOrElse(this.configuration, standaloneConfig::getPassword); } @@ -568,7 +587,7 @@ private RedisPassword getRedisPassword() { @Deprecated public void setPassword(String password) { - if (RedisConfiguration.isPasswordAware(configuration)) { + if (RedisConfiguration.isAuthenticationAware(configuration)) { ((WithPassword) configuration).setPassword(password); return; @@ -851,10 +870,12 @@ public RedisSentinelConnection getSentinelConnection() { private Jedis getActiveSentinel() { Assert.isTrue(RedisConfiguration.isSentinelConfiguration(configuration), "SentinelConfig must not be null!"); + SentinelConfiguration sentinelConfiguration = (SentinelConfiguration) configuration; - for (RedisNode node : ((SentinelConfiguration) configuration).getSentinels()) { + for (RedisNode node : sentinelConfiguration.getSentinels()) { Jedis jedis = new Jedis(node.getHost(), node.getPort(), getConnectTimeout(), getReadTimeout()); + sentinelConfiguration.getSentinelPassword().toOptional().map(String::new).ifPresent(jedis::auth); try { if (jedis.ping().equalsIgnoreCase("pong")) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index 3c67d8c787..ddc53b590f 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -788,6 +788,9 @@ public void setClientName(@Nullable String clientName) { this.getMutableConfiguration().setClientName(clientName); } + private Optional getRedisUsername() { + return RedisConfiguration.getUsernameOrElse(configuration, standaloneConfig::getUsername); + } /** * Returns the password used for authenticating with the Redis server. * @@ -812,7 +815,7 @@ private RedisPassword getRedisPassword() { @Deprecated public void setPassword(String password) { - if (RedisConfiguration.isPasswordAware(configuration)) { + if (RedisConfiguration.isAuthenticationAware(configuration)) { ((WithPassword) configuration).setPassword(password); return; @@ -1135,7 +1138,8 @@ private RedisURI createRedisURIAndApplySettings(String host, int port) { RedisURI.Builder builder = RedisURI.Builder.redis(host, port); - getRedisPassword().toOptional().ifPresent(builder::withPassword); + applyAuthentication(builder); + clientConfiguration.getClientName().ifPresent(builder::withClientName); builder.withDatabase(getDatabase()); @@ -1151,13 +1155,25 @@ private RedisURI createRedisSocketURIAndApplySettings(String socketPath) { RedisURI.Builder builder = RedisURI.Builder.socket(socketPath); - getRedisPassword().toOptional().ifPresent(builder::withPassword); + applyAuthentication(builder); builder.withDatabase(getDatabase()); builder.withTimeout(clientConfiguration.getCommandTimeout()); return builder.build(); } + private void applyAuthentication(RedisURI.Builder builder) { + + Optional username = getRedisUsername(); + if (username.isPresent()) { + // See https://github.com/lettuce-io/lettuce-core/issues/1404 + username.ifPresent( + it -> builder.withAuthentication(it, new String(getRedisPassword().toOptional().orElse(new char[0])))); + } else { + getRedisPassword().toOptional().ifPresent(builder::withPassword); + } + } + @Override public RedisSentinelConnection getSentinelConnection() { return new LettuceSentinelConnection(connectionProvider); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 44be5a7254..2d90e28b1c 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -18,16 +18,11 @@ import io.lettuce.core.*; import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag; -import io.lettuce.core.models.stream.PendingMessage; -import io.lettuce.core.models.stream.PendingMessages; -import io.lettuce.core.models.stream.PendingParser; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; import java.util.stream.Collectors; import org.springframework.core.convert.converter.Converter; @@ -69,9 +64,6 @@ import org.springframework.data.redis.connection.convert.ListConverter; import org.springframework.data.redis.connection.convert.LongToBooleanConverter; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; -import org.springframework.data.redis.connection.stream.Consumer; -import org.springframework.data.redis.connection.stream.PendingMessagesSummary; -import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; @@ -79,7 +71,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -657,15 +648,19 @@ public static RedisURI sentinelConfigurationToRedisURI(RedisSentinelConfiguratio RedisURI.Builder sentinelBuilder = RedisURI.Builder.redis(sentinel.getHost(), sentinel.getPort()); - if (sentinelPassword.isPresent()) { - sentinelBuilder.withPassword(sentinelPassword.get()); - } + sentinelPassword.toOptional().ifPresent(sentinelBuilder::withPassword); + builder.withSentinel(sentinelBuilder.build()); } + Optional username = sentinelConfiguration.getUsername(); RedisPassword password = sentinelConfiguration.getPassword(); - if (password.isPresent()) { - builder.withPassword(password.get()); + + if (username.isPresent()) { + // See https://github.com/lettuce-io/lettuce-core/issues/1404 + builder.withAuthentication(username.get(), new String(password.toOptional().orElse(new char[0]))); + } else { + password.toOptional().ifPresent(builder::withPassword); } builder.withSentinelMasterId(sentinelConfiguration.getMaster().getName()); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java index fca79aec86..ef3d0e6807 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java @@ -26,6 +26,7 @@ import org.springframework.data.redis.connection.ReactiveScriptingCommands; import org.springframework.data.redis.connection.ReturnType; +import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; /** @@ -80,7 +81,7 @@ public Mono scriptLoad(ByteBuffer script) { Assert.notNull(script, "Script must not be null!"); - return connection.execute(cmd -> cmd.scriptLoad(script)).next(); + return connection.execute(cmd -> cmd.scriptLoad(ByteUtils.getBytes(script))).next(); } /* diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java index 2cb940c0a3..cf1baeb95a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommands.java @@ -21,15 +21,16 @@ import io.lettuce.core.XReadArgs; import io.lettuce.core.XReadArgs.StreamOffset; import io.lettuce.core.cluster.api.reactive.RedisClusterReactiveCommands; +import io.lettuce.core.models.stream.PendingMessage; import reactor.core.publisher.Flux; import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; @@ -204,7 +205,7 @@ public Flux> xGroup(Publisher new CommandResponse<>(command, Boolean.TRUE.equals(it) ? "OK" : "Error")); + .map(it -> new CommandResponse<>(command, "OK")); } if (command.getAction().equals(GroupCommandAction.DESTROY)) { @@ -243,25 +244,8 @@ public Flux> xPen return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null!"); - return cmd.xpending(command.getKey(), ByteUtils.getByteBuffer(command.getGroupName())).collectList().map(it -> { - - // begin - // {* hacking *} - // while (https://github.com/lettuce-io/lettuce-core/issues/1229 != resolved) begin - - ArrayList target = new ArrayList<>(it); - if (target.size() == 2 && target.get(1) instanceof List) { - target.add(1, null); - target.add(1, null); - } - while (target.size() < 4) { - target.add(null); - } - - // end. - // end. - - return StreamConverters.toPendingMessagesInfo(command.getGroupName(), target); + return cmd.xpending(command.getKey(), ByteUtils.getByteBuffer(command.getGroupName())).map(it -> { + return StreamConverters.toPendingMessagesInfo(command.getGroupName(), it); }).map(value -> new CommandResponse<>(command, value)); })); } @@ -282,7 +266,7 @@ public Flux> xPending( io.lettuce.core.Limit limit = command.isLimited() ? io.lettuce.core.Limit.from(command.getCount()) : io.lettuce.core.Limit.unlimited(); - Flux publisher = command.hasConsumer() ? cmd.xpending(command.getKey(), + Flux publisher = command.hasConsumer() ? cmd.xpending(command.getKey(), io.lettuce.core.Consumer.from(groupName, ByteUtils.getByteBuffer(command.getConsumerName())), range, limit) : cmd.xpending(command.getKey(), groupName, range, limit); @@ -353,7 +337,7 @@ private static Flux doRead(ReadCommand command, StreamReadOpti .map(it -> StreamRecords.newRecord().in(it.getStream()).withId(it.getId()).ofBuffer(it.getBody())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveStreamCommands#xInfo(org.reactivestreams.Publisher) */ @@ -370,7 +354,7 @@ public Flux> xInfo(Publisher>> xInfoGroups(Publish })); } - /* + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveStreamCommands#xInfoConsumers(org.reactivestreams.Publisher) */ diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java index 3290712ffc..2a3f0036be 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStreamCommands.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.Function; import org.springframework.dao.DataAccessException; @@ -267,14 +268,16 @@ public Boolean xGroupDelConsumer(byte[] key, Consumer consumer) { io.lettuce.core.Consumer lettuceConsumer = toConsumer(consumer); if (isPipelined()) { - pipeline(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer))); + pipeline(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer), + Objects::nonNull)); return null; } if (isQueueing()) { - transaction(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer))); + transaction(connection.newLettuceResult(getAsyncConnection().xgroupDelconsumer(key, lettuceConsumer), + Objects::nonNull)); return null; } - return getConnection().xgroupDelconsumer(key, lettuceConsumer); + return Objects.nonNull(getConnection().xgroupDelconsumer(key, lettuceConsumer)); } catch (Exception ex) { throw convertLettuceAccessException(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java index 75e126905d..733f107951 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/StreamConverters.java @@ -20,7 +20,6 @@ import io.lettuce.core.XReadArgs; import io.lettuce.core.models.stream.PendingMessage; import io.lettuce.core.models.stream.PendingMessages; -import io.lettuce.core.models.stream.PendingParser; import java.nio.ByteBuffer; import java.time.Duration; @@ -51,19 +50,17 @@ * @author Christoph Strobl * @since 2.2 */ -@SuppressWarnings({ "unchecked", "rawtypes" }) +@SuppressWarnings({ "rawtypes" }) class StreamConverters { private static final Converter>, List> MESSAGEs_TO_IDs = new ListConverter<>( messageToIdConverter()); - private static final BiFunction, String, org.springframework.data.redis.connection.stream.PendingMessages> PENDING_MESSAGES_CONVERTER = ( + private static final BiFunction, String, org.springframework.data.redis.connection.stream.PendingMessages> PENDING_MESSAGES_CONVERTER = ( source, groupName) -> { - List target = source.stream().map(StreamConverters::preConvertNativeValues).collect(Collectors.toList()); - List pendingMessages = PendingParser.parseRange(target); - List messages = pendingMessages.stream() + List messages = source.stream() .map(it -> { RecordId id = RecordId.of(it.getId()); @@ -78,17 +75,15 @@ class StreamConverters { }; - private static final BiFunction, String, PendingMessagesSummary> PENDING_MESSAGES_SUMMARY_CONVERTER = ( + private static final BiFunction PENDING_MESSAGES_SUMMARY_CONVERTER = ( source, groupName) -> { - List target = source.stream().map(StreamConverters::preConvertNativeValues).collect(Collectors.toList()); + org.springframework.data.domain.Range range = source.getMessageIds().isUnbounded() + ? org.springframework.data.domain.Range.unbounded() + : org.springframework.data.domain.Range.open(source.getMessageIds().getLower().getValue(), + source.getMessageIds().getUpper().getValue()); - PendingMessages pendingMessages = PendingParser.parse(target); - org.springframework.data.domain.Range range = org.springframework.data.domain.Range.open( - pendingMessages.getMessageIds().getLower().getValue(), pendingMessages.getMessageIds().getUpper().getValue()); - - return new PendingMessagesSummary(groupName, pendingMessages.getCount(), range, - pendingMessages.getConsumerMessageCount()); + return new PendingMessagesSummary(groupName, source.getCount(), range, source.getConsumerMessageCount()); }; /** @@ -138,7 +133,7 @@ static Converter>, List> messagesTo * @since 2.3 */ static org.springframework.data.redis.connection.stream.PendingMessages toPendingMessages(String groupName, - org.springframework.data.domain.Range range, List source) { + org.springframework.data.domain.Range range, List source) { return PENDING_MESSAGES_CONVERTER.apply(source, groupName).withinRange(range); } @@ -150,7 +145,7 @@ static org.springframework.data.redis.connection.stream.PendingMessages toPendin * @return * @since 2.3 */ - static PendingMessagesSummary toPendingMessagesInfo(String groupName, List source) { + static PendingMessagesSummary toPendingMessagesInfo(String groupName, PendingMessages source) { return PENDING_MESSAGES_SUMMARY_CONVERTER.apply(source, groupName); } diff --git a/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java b/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java index 13010faea2..50d5a14404 100644 --- a/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java +++ b/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java @@ -41,6 +41,7 @@ public class RedisTestProfileValueSource implements ProfileValueSource { private static final String REDIS_30 = "3.0"; private static final String REDIS_32 = "3.2"; private static final String REDIS_50 = "5.0"; + private static final String REDIS_60 = "6.0"; private static final String REDIS_VERSION_KEY = "redisVersion"; private static RedisTestProfileValueSource INSTANCE; @@ -97,6 +98,10 @@ public String get(String key) { return System.getProperty(key); } + if (redisVersion.compareTo(RedisVersionUtils.parseVersion(REDIS_60)) >= 0) { + return REDIS_60; + } + if (redisVersion.compareTo(RedisVersionUtils.parseVersion(REDIS_50)) >= 0) { return REDIS_50; } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java new file mode 100644 index 0000000000..4d52f83dcf --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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.springframework.data.redis.connection.jedis; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.redis.RedisTestProfileValueSource; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; + +/** + * Integration tests for Redis 6 ACL. + * + * @author Mark Paluch + */ +public class JedisAclIntegrationTests { + + @Before + public void before() { + assumeTrue(RedisTestProfileValueSource.atLeast("redisVersion", "6.0")); + } + + @Test + public void shouldConnectWithDefaultAuthentication() { + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6382); + standaloneConfiguration.setPassword("foobared"); + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(standaloneConfiguration); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1046 + public void shouldConnectStandaloneWithAclAuthentication() { + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6382); + standaloneConfiguration.setUsername("spring"); + standaloneConfiguration.setPassword("data"); + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(standaloneConfiguration); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1145 + public void shouldConnectSentinelWithAclAuthentication() throws IOException { + + // Note: As per https://github.com/redis/redis/issues/7708, Sentinel does not support ACL authentication yet. + + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration("mymaster", + Collections.singleton("localhost:26382")); + sentinelConfiguration.setSentinelPassword("foobared"); + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(sentinelConfiguration); + connectionFactory.afterPropertiesSet(); + + RedisSentinelConnection connection = connectionFactory.getSentinelConnection(); + + assertThat(connection.masters()).isNotEmpty(); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1046 + public void shouldConnectStandaloneWithAclAuthenticationAndPooling() { + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6382); + standaloneConfiguration.setUsername("spring"); + standaloneConfiguration.setPassword("data"); + + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(standaloneConfiguration); + connectionFactory.setUsePool(true); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/AuthenticatingRedisClientTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/AuthenticatingRedisClientTests.java index d397d9643e..cfe62e23a1 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/AuthenticatingRedisClientTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/AuthenticatingRedisClientTests.java @@ -22,24 +22,22 @@ import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; /** - * Integration test of {@link AuthenticatingRedisClient}. Enable requirepass and comment out the @Ignore to run. + * Integration test of {@link AuthenticatingRedisClient}. * * @author Jennifer Hickey * @author Thomas Darimont * @author Christoph Strobl */ -@Ignore("Redis must have requirepass set to run this test") public class AuthenticatingRedisClientTests { private RedisClient client; @Before public void setUp() { - client = new AuthenticatingRedisClient("localhost", "foo"); + client = new AuthenticatingRedisClient("localhost", 6382, "foobared"); } @After @@ -63,7 +61,7 @@ public void connectWithInvalidPassword() { client.shutdown(); } - RedisClient badClient = new AuthenticatingRedisClient("localhost", "notthepassword"); + RedisClient badClient = new AuthenticatingRedisClient("localhost", 6382, "notthepassword"); badClient.connect(); } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolTests.java index a5d137d38e..02ad4b1ecd 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolTests.java @@ -171,16 +171,6 @@ public void testCreateWithDbIndexInvalid() { pool.getResource(); } - @Test(expected = PoolException.class) - public void testCreateWithPasswordNoPassword() { - - pool = new DefaultLettucePool(SettingsUtils.getHost(), SettingsUtils.getPort()); - pool.setClientResources(LettuceTestClientResources.getSharedClientResources()); - pool.setPassword("notthepassword"); - pool.afterPropertiesSet(); - pool.getResource(); - } - @Ignore("Redis must have requirepass set to run this test") @Test public void testCreatePassword() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java new file mode 100644 index 0000000000..7a1a056fd0 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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.springframework.data.redis.connection.lettuce; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import io.lettuce.core.ClientOptions; +import io.lettuce.core.protocol.ProtocolVersion; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.data.redis.RedisTestProfileValueSource; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration; + +/** + * Integration tests for Redis 6 ACL. + * + * @author Mark Paluch + */ +public class LettuceAclIntegrationTests { + + @Before + public void before() { + assumeTrue(RedisTestProfileValueSource.atLeast("redisVersion", "6.0")); + } + + @Test // DATAREDIS-1046 + public void shouldConnectWithDefaultAuthentication() { + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6382); + standaloneConfiguration.setPassword("foobared"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(standaloneConfiguration); + connectionFactory.setClientResources(LettuceTestClientResources.getSharedClientResources()); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1046 + public void shouldConnectStandaloneWithAclAuthentication() { + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6382); + standaloneConfiguration.setUsername("spring"); + standaloneConfiguration.setPassword("data"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(standaloneConfiguration); + connectionFactory.setClientResources(LettuceTestClientResources.getSharedClientResources()); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1145 + public void shouldConnectSentinelWithAuthentication() throws IOException { + + // Note: As per https://github.com/redis/redis/issues/7708, Sentinel does not support ACL authentication yet. + + LettuceClientConfiguration configuration = LettuceClientConfiguration.builder() + .clientResources(LettuceTestClientResources.getSharedClientResources()) + .clientOptions(ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).build()).build(); + + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration("mymaster", + Collections.singleton("localhost:26382")); + sentinelConfiguration.setSentinelPassword("foobared"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(sentinelConfiguration, configuration); + connectionFactory.afterPropertiesSet(); + + RedisSentinelConnection connection = connectionFactory.getSentinelConnection(); + + assertThat(connection.masters()).isNotEmpty(); + connection.close(); + + connectionFactory.destroy(); + } + + @Test // DATAREDIS-1046 + @Ignore("https://github.com/lettuce-io/lettuce-core/issues/1406") + public void shouldConnectMasterReplicaWithAclAuthentication() { + + RedisStaticMasterReplicaConfiguration masterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration( + "localhost", 6382); + masterReplicaConfiguration.setUsername("spring"); + masterReplicaConfiguration.setPassword("data"); + + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(masterReplicaConfiguration); + connectionFactory.setClientResources(LettuceTestClientResources.getSharedClientResources()); + connectionFactory.afterPropertiesSet(); + + RedisConnection connection = connectionFactory.getConnection(); + + assertThat(connection.ping()).isEqualTo("PONG"); + connection.close(); + + connectionFactory.destroy(); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java index 0963998080..0c99744779 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java @@ -22,12 +22,14 @@ import io.lettuce.core.KqueueProvider; import io.lettuce.core.ReadFrom; import io.lettuce.core.RedisException; +import io.lettuce.core.RedisFuture; import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.api.reactive.BaseRedisReactiveCommands; import reactor.test.StepVerifier; import java.io.File; import java.time.Duration; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -230,11 +232,12 @@ public void testDisableSharedConnection() throws Exception { assertThat(conn2.isClosed()).isTrue(); // Give some time for native connection to asynchronously close Thread.sleep(100); + RedisFuture future = ((RedisAsyncCommands) conn2.getNativeConnection()).ping(); try { - ((RedisAsyncCommands) conn2.getNativeConnection()).ping(); + future.get(); fail("The native connection should be closed"); - } catch (RedisException e) { - // expected + } catch (ExecutionException e) { + // expected, Lettuce async failures are signalled on the Future } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java index 2c842dd6ce..78f9822bd6 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.*; import static org.junit.Assume.*; -import static org.springframework.data.redis.SpinBarrier.*; import io.lettuce.core.api.async.RedisAsyncCommands; diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsTests.java index 57b6ec78cb..61773cb71d 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStreamCommandsTests.java @@ -50,9 +50,7 @@ public class LettuceReactiveStreamCommandsTests extends LettuceReactiveCommandsT @Before public void before() { - - // TODO: Upgrade to 5.0 - assumeTrue(RedisTestProfileValueSource.atLeast("redisVersion", "4.9")); + assumeTrue(RedisTestProfileValueSource.atLeast("redisVersion", "5.0")); } @Test // DATAREDIS-864 diff --git a/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java index b68287821f..eb4ec8baba 100644 --- a/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java +++ b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java @@ -15,10 +15,13 @@ */ package org.springframework.data.redis.test.util; +import io.lettuce.core.ClientOptions; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; +import io.lettuce.core.protocol.ProtocolVersion; import org.junit.rules.ExternalResource; + import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources; @@ -44,6 +47,8 @@ protected void before() { client = RedisClient.create(LettuceTestClientResources.getSharedClientResources(), RedisURI.builder().withHost(host).withPort(port).build()); + client.setOptions( + ClientOptions.builder().protocolVersion(ProtocolVersion.RESP2).pingBeforeActivateConnection(false).build()); } @Override diff --git a/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java index 59c64a030a..7f5eabbd0a 100644 --- a/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java +++ b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java @@ -15,12 +15,15 @@ */ package org.springframework.data.redis.test.util; -import org.junit.rules.ExternalResource; -import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources; - import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.ClusterClientOptions; import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.protocol.ProtocolVersion; + +import org.junit.rules.ExternalResource; + +import org.springframework.data.redis.connection.lettuce.LettuceTestClientResources; /** * @author Christoph Strobl @@ -44,6 +47,8 @@ protected void before() { client = RedisClusterClient.create(LettuceTestClientResources.getSharedClientResources(), RedisURI.builder().withHost(host).withPort(port).build()); + client.setOptions(ClusterClientOptions.builder().protocolVersion(ProtocolVersion.RESP2) + .pingBeforeActivateConnection(false).build()); } @Override