diff --git a/Makefile b/Makefile index 07f7c0ce9..3eb57118d 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 @@ -150,6 +168,9 @@ 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: redis-stop sentinel-stop cluster-stop test: diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 5ae24df58..f0f19afa2 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,9 +7,9 @@ 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. [[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/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java index 236ff1c2a..7947da1f8 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.Collections; 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 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon } } + /* + * (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 523d32af0..e860cf881 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 @@ package org.springframework.data.redis.connection; 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 @@ public interface RedisConfiguration { /** * 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 @@ public interface RedisConfiguration { * @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 @@ public interface RedisConfiguration { /** * @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 @@ public interface RedisConfiguration { * {@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 @@ public interface RedisConfiguration { */ 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 @@ public interface RedisConfiguration { 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 @@ public interface RedisConfiguration { 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 406e7bb72..8313b7a76 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.Collections; 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 class RedisSentinelConfiguration implements RedisConfiguration, SentinelC 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 c2b433624..93118a572 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 class RedisSocketConfiguration implements RedisConfiguration, DomainSocke 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 12d2a96a0..058b32af1 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 class RedisStandaloneConfiguration 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 c7a1ad293..d31f5939e 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 @@ package org.springframework.data.redis.connection; 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 class RedisStaticMasterReplicaConfiguration implements RedisConfiguration 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 d65f8ca09..998a35cf0 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 class JedisConnectionFactory implements InitializingBean, DisposableBean, clientConfiguration.getHostnameVerifier().orElse(null)); getRedisPassword().map(String::new).ifPresent(shardInfo::setPassword); + getRedisUsername().ifPresent(shardInfo::setUser); int readTimeout = getReadTimeout(); @@ -369,9 +370,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, */ protected Pool createRedisSentinelPool(RedisSentinelConfiguration config) { - GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(); + GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig(); + return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()), - poolConfig, getConnectTimeout(), getReadTimeout(), getPassword(), getDatabase(), getClientName()); + poolConfig, getConnectTimeout(), getReadTimeout(), getUsername(), getPassword(), getDatabase(), + getClientName()); } /** @@ -383,7 +386,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, 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 +417,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, * @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 +428,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, 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 +547,16 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, 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 +567,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, 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 +585,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, @Deprecated public void setPassword(String password) { - if (RedisConfiguration.isPasswordAware(configuration)) { + if (RedisConfiguration.isAuthenticationAware(configuration)) { ((WithPassword) configuration).setPassword(password); return; 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 3c67d8c78..ddc53b590 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 class LettuceConnectionFactory 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 @@ public class LettuceConnectionFactory @Deprecated public void setPassword(String password) { - if (RedisConfiguration.isPasswordAware(configuration)) { + if (RedisConfiguration.isAuthenticationAware(configuration)) { ((WithPassword) configuration).setPassword(password); return; @@ -1135,7 +1138,8 @@ public class LettuceConnectionFactory 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 @@ public class LettuceConnectionFactory 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 44be5a725..2d90e28b1 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 @@ package org.springframework.data.redis.connection.lettuce; 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.Converters; 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.data.redis.util.ByteUtils; 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 @@ abstract public class LettuceConverters extends Converters { 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/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java b/src/test/java/org/springframework/data/redis/RedisTestProfileValueSource.java index 13010faea..50d5a1440 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 class RedisTestProfileValueSource implements ProfileValueSource { 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 000000000..6fe790687 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisAclIntegrationTests.java @@ -0,0 +1,93 @@ +/* + * 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 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.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-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 d397d9643..cfe62e23a 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 io.lettuce.core.pubsub.StatefulRedisPubSubConnection; 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 class AuthenticatingRedisClientTests { 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 a5d137d38..02ad4b1ec 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 class DefaultLettucePoolTests { 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 000000000..e14e58e34 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceAclIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * 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 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.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-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(); + } +}