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 f8cac2cb5..dae6dc168 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.util.Assert; * * @author Christoph Strobl * @author Luis De Bello + * @author Vikas Garg * @since 2.1 */ public interface RedisConfiguration { @@ -438,6 +439,21 @@ public interface RedisConfiguration { * @since 2.2.2 */ RedisPassword getSentinelPassword(); + + /** + * Create and set a username with the given {@link String}. Requires Redis 6 or newer. + * + * @param sentinelUsername the username for sentinel. + */ + void setSentinelUsername(@Nullable String sentinelUsername); + + /** + * Get the username to use when connecting. + * + * @return {@literal null} if none set. + */ + @Nullable + String getSentinelUsername(); } /** 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 a758f8a36..219c77b3c 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.util.StringUtils; * @author Christoph Strobl * @author Thomas Darimont * @author Mark Paluch + * @author Vikas Garg * @since 1.4 */ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelConfiguration { @@ -46,12 +47,14 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC private static final String REDIS_SENTINEL_MASTER_CONFIG_PROPERTY = "spring.redis.sentinel.master"; private static final String REDIS_SENTINEL_NODES_CONFIG_PROPERTY = "spring.redis.sentinel.nodes"; private static final String REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY = "spring.redis.sentinel.password"; + private static final String REDIS_SENTINEL_USERNAME_CONFIG_PROPERTY = "spring.redis.sentinel.username"; private @Nullable NamedNode master; private Set sentinels; private int database; private @Nullable String dataNodeUsername = null; + private @Nullable String sentinelUsername = null; private RedisPassword dataNodePassword = RedisPassword.none(); private RedisPassword sentinelPassword = RedisPassword.none(); @@ -107,6 +110,10 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC if (propertySource.containsProperty(REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY)) { this.setSentinelPassword(propertySource.getProperty(REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY).toString()); } + + if (propertySource.containsProperty(REDIS_SENTINEL_USERNAME_CONFIG_PROPERTY)) { + this.setSentinelUsername(propertySource.getProperty(REDIS_SENTINEL_USERNAME_CONFIG_PROPERTY).toString()); + } } /** @@ -270,6 +277,25 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC this.dataNodePassword = password; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.SentinelConfiguration#getSentinelUsername() + */ + @Nullable + @Override + public String getSentinelUsername() { + return this.sentinelUsername; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConfiguration.SentinelConfiguration#setSentinelUsername(String) + */ + @Override + public void setSentinelUsername(@Nullable String sentinelUsername) { + this.sentinelUsername = sentinelUsername; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisConfiguration.SentinelConfiguration#setSentinelPassword(org.springframework.data.redis.connection.RedisPassword) 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 d0ef93f88..387a877df 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 @@ -82,6 +82,7 @@ import org.springframework.util.StringUtils; * @author Ninad Divadkar * @author dengliming * @author Chris Bono + * @author Vikas Garg */ public abstract class LettuceConverters extends Converters { @@ -512,7 +513,13 @@ public abstract class LettuceConverters extends Converters { RedisURI.Builder sentinelBuilder = RedisURI.Builder.redis(sentinel.getHost(), sentinel.getPort()); - sentinelPassword.toOptional().ifPresent(sentinelBuilder::withPassword); + String sentinelUsername = sentinelConfiguration.getSentinelUsername(); + if (StringUtils.hasText(sentinelUsername) && sentinelPassword.isPresent()) { + // See https://github.com/lettuce-io/lettuce-core/issues/1404 + sentinelBuilder.withAuthentication(sentinelUsername, new String(sentinelPassword.toOptional().orElse((new char[0])))); + } else { + sentinelPassword.toOptional().ifPresent(sentinelBuilder::withPassword); + } builder.withSentinel(sentinelBuilder.build()); } diff --git a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java index 20aaae28f..e90dc6c73 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java @@ -29,6 +29,7 @@ import org.springframework.util.StringUtils; /** * @author Christoph Strobl + * @author Vikas Garg */ class RedisSentinelConfigurationUnitTests { @@ -67,20 +68,20 @@ class RedisSentinelConfigurationUnitTests { @Test // DATAREDIS-372 void shouldThrowExceptionWhenListOfHostAndPortIsNull() { assertThatIllegalArgumentException() - .isThrownBy(() -> new RedisSentinelConfiguration("mymaster", Collections. singleton(null))); + .isThrownBy(() -> new RedisSentinelConfiguration("mymaster", Collections.singleton(null))); } @Test // DATAREDIS-372 void shouldNotFailWhenListOfHostAndPortIsEmpty() { - RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections. emptySet()); + RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections.emptySet()); assertThat(config.getSentinels()).isEmpty(); } @Test // DATAREDIS-372 void shouldThrowExceptionGivenNullPropertySource() { - assertThatIllegalArgumentException().isThrownBy(() -> new RedisSentinelConfiguration((PropertySource) null)); + assertThatIllegalArgumentException().isThrownBy(() -> new RedisSentinelConfiguration(null)); } @Test // DATAREDIS-372 @@ -93,7 +94,7 @@ class RedisSentinelConfigurationUnitTests { } @Test // DATAREDIS-372 - void shouldBeCreatedCorrecltyGivenValidPropertySourceWithMasterAndSingleHostPort() { + void shouldBeCreatedCorrectlyGivenValidPropertySourceWithMasterAndSingleHostPort() { MockPropertySource propertySource = new MockPropertySource(); propertySource.setProperty("spring.redis.sentinel.master", "myMaster"); @@ -101,13 +102,14 @@ class RedisSentinelConfigurationUnitTests { RedisSentinelConfiguration config = new RedisSentinelConfiguration(propertySource); + assertThat(config.getMaster()).isNotNull(); assertThat(config.getMaster().getName()).isEqualTo("myMaster"); assertThat(config.getSentinels()).hasSize(1); assertThat(config.getSentinels()).contains(new RedisNode("127.0.0.1", 123)); } @Test // DATAREDIS-372 - void shouldBeCreatedCorrecltyGivenValidPropertySourceWithMasterAndMultipleHostPort() { + void shouldBeCreatedCorrectlyGivenValidPropertySourceWithMasterAndMultipleHostPort() { MockPropertySource propertySource = new MockPropertySource(); propertySource.setProperty("spring.redis.sentinel.master", "myMaster"); @@ -132,6 +134,16 @@ class RedisSentinelConfigurationUnitTests { assertThat(configuration.getSentinelPassword()).isEqualTo(RedisPassword.none()); } + @Test + void dataNodeUsernameDoesNotAffectSentinelUsername() { + RedisSentinelConfiguration configuration = new RedisSentinelConfiguration("myMaster", + Collections.singleton(HOST_AND_PORT_1)); + configuration.setSentinelUsername("sentinel-admin"); + configuration.setUsername("app"); + + assertThat(configuration.getSentinelUsername()).isEqualTo("sentinel-admin"); + } + @Test // DATAREDIS-1060 void readSentinelPasswordFromConfigProperty() { @@ -145,4 +157,18 @@ class RedisSentinelConfigurationUnitTests { assertThat(config.getSentinelPassword()).isEqualTo(RedisPassword.of("computer-says-no")); assertThat(config.getSentinels()).hasSize(1).contains(new RedisNode("127.0.0.1", 123)); } + + @Test + void readSentinelUsernameFromConfigProperty() { + + MockPropertySource propertySource = new MockPropertySource(); + propertySource.setProperty("spring.redis.sentinel.master", "myMaster"); + propertySource.setProperty("spring.redis.sentinel.nodes", HOST_AND_PORT_1); + propertySource.setProperty("spring.redis.sentinel.username", "sentinel-admin"); + + RedisSentinelConfiguration config = new RedisSentinelConfiguration(propertySource); + + assertThat(config.getSentinelUsername()).isEqualTo(RedisPassword.of("sentinel-admin")); + assertThat(config.getSentinels()).hasSize(1).contains(new RedisNode("127.0.0.1", 123)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 917fd1e80..ff835dd5c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -37,20 +37,23 @@ import org.junit.jupiter.api.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands; -import org.springframework.data.redis.connection.jedis.JedisConverters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; -import redis.clients.jedis.params.SetParams; /** * @author Christoph Strobl + * @author Vikas Garg */ class LettuceConvertersUnitTests { private static final String CLIENT_ALL_SINGLE_LINE_RESPONSE = "addr=127.0.0.1:60311 fd=6 name= age=4059 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client"; + private static final String MASTER_NAME = "mymaster"; + @Test // DATAREDIS-268 void convertingEmptyStringToListOfRedisClientInfoShouldReturnEmptyList() { assertThat(LettuceConverters.toListOfRedisClientInformation("")) @@ -75,12 +78,12 @@ class LettuceConvertersUnitTests { } @Test // DATAREDIS-315 - void partitionsToClusterNodesShouldReturnEmptyCollectionWhenPartionsDoesNotContainElements() { + void partitionsToClusterNodesShouldReturnEmptyCollectionWhenPartitionsDoesNotContainElements() { assertThat(LettuceConverters.partitionsToClusterNodes(new Partitions())).isNotNull(); } @Test // DATAREDIS-315 - void partitionsToClusterNodesShouldConvertPartitionCorrctly() { + void partitionsToClusterNodesShouldConvertPartitionCorrectly() { Partitions partitions = new Partitions(); @@ -89,7 +92,7 @@ class LettuceConvertersUnitTests { partition.setConnected(true); partition.setFlags(new HashSet<>(Arrays.asList(NodeFlag.MASTER, NodeFlag.MYSELF))); partition.setUri(RedisURI.create("redis://" + CLUSTER_HOST + ":" + MASTER_NODE_1_PORT)); - partition.setSlots(Arrays. asList(1, 2, 3, 4, 5)); + partition.setSlots(Arrays.asList(1, 2, 3, 4, 5)); partitions.add(partition); @@ -252,4 +255,58 @@ class LettuceConvertersUnitTests { assertThatCommandArgument(LettuceConverters.toGetExArgs(Expiration.unixTimestamp(10, TimeUnit.MILLISECONDS))) .isEqualTo(new GetExArgs().pxAt(10)); } + + @Test + void sentinelConfigurationWithAuth() { + RedisPassword password = RedisPassword.of("88888888-8x8-getting-creative-now"); + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration() + .master(MASTER_NAME) + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + sentinelConfiguration.setSentinelUsername("admin"); + sentinelConfiguration.setSentinelPassword(password); + sentinelConfiguration.setUsername("app"); + sentinelConfiguration.setPassword(password); + RedisURI redisURI = LettuceConverters.sentinelConfigurationToRedisURI(sentinelConfiguration); + assertThat(redisURI.getUsername()).isEqualTo("app"); + redisURI.getSentinels().forEach(sentinel -> { + assertThat(sentinel.getUsername()).isEqualTo("admin"); + }); + } + + @Test + void sentinelConfigurationSetSentinelPasswordIfUsernameNotPresent() { + RedisPassword password = RedisPassword.of("88888888-8x8-getting-creative-now"); + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration() + .master(MASTER_NAME) + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + sentinelConfiguration.setSentinelPassword(password); + sentinelConfiguration.setUsername("app"); + sentinelConfiguration.setPassword(password); + RedisURI redisURI = LettuceConverters.sentinelConfigurationToRedisURI(sentinelConfiguration); + assertThat(redisURI.getUsername()).isEqualTo("app"); + redisURI.getSentinels().forEach(sentinel -> { + assertThat(sentinel.getUsername()).isNull(); + assertThat(sentinel.getPassword()).isNotNull(); + }); + } + + @Test + void sentinelConfigurationShouldNotSetSentinelAuthIfUsernameIsPresentWithNoPassword() { + RedisPassword password = RedisPassword.of("88888888-8x8-getting-creative-now"); + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration() + .master(MASTER_NAME) + .sentinel("127.0.0.1", 26379) + .sentinel("127.0.0.1", 26380); + sentinelConfiguration.setSentinelUsername("admin"); + sentinelConfiguration.setUsername("app"); + sentinelConfiguration.setPassword(password); + RedisURI redisURI = LettuceConverters.sentinelConfigurationToRedisURI(sentinelConfiguration); + assertThat(redisURI.getUsername()).isEqualTo("app"); + redisURI.getSentinels().forEach(sentinel -> { + assertThat(sentinel.getUsername()).isNull(); + assertThat(sentinel.getPassword()).isNull(); + }); + } }