Add support for Lettuce's 6.2 RedisCredentialsProvider.
We now support construction of RedisCredentialsProvider through LettuceClientConfiguration and RedisCredentialsProviderFactory. The default implementation adapts credentials configured in RedisConfiguration objects. Closes: #2376 Original Pull Request: #2387
This commit is contained in:
committed by
Christoph Strobl
parent
c3ee0d8b3c
commit
8f37abe950
@@ -41,13 +41,15 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration {
|
||||
private final Optional<ClientOptions> clientOptions;
|
||||
private final Optional<String> clientName;
|
||||
private final Optional<ReadFrom> readFrom;
|
||||
private final Optional<RedisCredentialsProviderFactory> redisCredentialsProviderFactory;
|
||||
private final Duration timeout;
|
||||
private final Duration shutdownTimeout;
|
||||
private final Duration shutdownQuietPeriod;
|
||||
|
||||
DefaultLettuceClientConfiguration(boolean useSsl, boolean verifyPeer, boolean startTls,
|
||||
@Nullable ClientResources clientResources, @Nullable ClientOptions clientOptions, @Nullable String clientName,
|
||||
@Nullable ReadFrom readFrom, Duration timeout, Duration shutdownTimeout, @Nullable Duration shutdownQuietPeriod) {
|
||||
@Nullable ReadFrom readFrom, @Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory,
|
||||
Duration timeout, Duration shutdownTimeout, @Nullable Duration shutdownQuietPeriod) {
|
||||
|
||||
this.useSsl = useSsl;
|
||||
this.verifyPeer = verifyPeer;
|
||||
@@ -56,6 +58,7 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration {
|
||||
this.clientOptions = Optional.ofNullable(clientOptions);
|
||||
this.clientName = Optional.ofNullable(clientName);
|
||||
this.readFrom = Optional.ofNullable(readFrom);
|
||||
this.redisCredentialsProviderFactory = Optional.ofNullable(redisCredentialsProviderFactory);
|
||||
this.timeout = timeout;
|
||||
this.shutdownTimeout = shutdownTimeout;
|
||||
this.shutdownQuietPeriod = shutdownQuietPeriod != null ? shutdownQuietPeriod : shutdownTimeout;
|
||||
@@ -96,6 +99,11 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration {
|
||||
return readFrom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
|
||||
return redisCredentialsProviderFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getCommandTimeout() {
|
||||
return timeout;
|
||||
|
||||
@@ -79,6 +79,11 @@ class DefaultLettucePoolingClientConfiguration implements LettucePoolingClientCo
|
||||
return clientConfiguration.getReadFrom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
|
||||
return clientConfiguration.getRedisCredentialsProviderFactory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Duration getCommandTimeout() {
|
||||
return clientConfiguration.getCommandTimeout();
|
||||
|
||||
@@ -94,6 +94,12 @@ public interface LettuceClientConfiguration {
|
||||
*/
|
||||
Optional<ReadFrom> getReadFrom();
|
||||
|
||||
/**
|
||||
* @return the optional {@link RedisCredentialsProviderFactory}.
|
||||
* @since 3.0
|
||||
*/
|
||||
Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory();
|
||||
|
||||
/**
|
||||
* @return the timeout.
|
||||
*/
|
||||
@@ -166,6 +172,7 @@ public interface LettuceClientConfiguration {
|
||||
ClientOptions clientOptions = ClientOptions.builder().timeoutOptions(TimeoutOptions.enabled()).build();
|
||||
@Nullable String clientName;
|
||||
@Nullable ReadFrom readFrom;
|
||||
@Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory;
|
||||
Duration timeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT);
|
||||
Duration shutdownTimeout = Duration.ofMillis(100);
|
||||
@Nullable Duration shutdownQuietPeriod;
|
||||
@@ -242,7 +249,7 @@ public interface LettuceClientConfiguration {
|
||||
*
|
||||
* @param readFrom must not be {@literal null}.
|
||||
* @return {@literal this} builder.
|
||||
* @throws IllegalArgumentException if clientOptions is {@literal null}.
|
||||
* @throws IllegalArgumentException if readFrom is {@literal null}.
|
||||
* @since 2.1
|
||||
*/
|
||||
public LettuceClientConfigurationBuilder readFrom(ReadFrom readFrom) {
|
||||
@@ -253,6 +260,24 @@ public interface LettuceClientConfiguration {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link RedisCredentialsProviderFactory} to obtain {@link io.lettuce.core.RedisCredentialsProvider}
|
||||
* instances to support credential rotation.
|
||||
*
|
||||
* @param redisCredentialsProviderFactory must not be {@literal null}.
|
||||
* @return {@literal this} builder.
|
||||
* @throws IllegalArgumentException if redisCredentialsProviderFactory is {@literal null}.
|
||||
* @since 3.0
|
||||
*/
|
||||
public LettuceClientConfigurationBuilder redisCredentialsProviderFactory(
|
||||
RedisCredentialsProviderFactory redisCredentialsProviderFactory) {
|
||||
|
||||
Assert.notNull(redisCredentialsProviderFactory, "RedisCredentialsProviderFactory must not be null");
|
||||
|
||||
this.redisCredentialsProviderFactory = redisCredentialsProviderFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@code clientName} to be set with {@code CLIENT SETNAME}.
|
||||
*
|
||||
@@ -323,7 +348,7 @@ public interface LettuceClientConfiguration {
|
||||
public LettuceClientConfiguration build() {
|
||||
|
||||
return new DefaultLettuceClientConfiguration(useSsl, verifyPeer, startTls, clientResources, clientOptions,
|
||||
clientName, readFrom, timeout, shutdownTimeout, shutdownQuietPeriod);
|
||||
clientName, readFrom, redisCredentialsProviderFactory, timeout, shutdownTimeout, shutdownQuietPeriod);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import io.lettuce.core.ClientOptions;
|
||||
import io.lettuce.core.ReadFrom;
|
||||
import io.lettuce.core.RedisClient;
|
||||
import io.lettuce.core.RedisConnectionException;
|
||||
import io.lettuce.core.RedisCredentialsProvider;
|
||||
import io.lettuce.core.RedisURI;
|
||||
import io.lettuce.core.api.StatefulConnection;
|
||||
import io.lettuce.core.api.StatefulRedisConnection;
|
||||
@@ -1216,6 +1217,15 @@ public class LettuceConnectionFactory
|
||||
|
||||
redisUri.setDatabase(getDatabase());
|
||||
|
||||
clientConfiguration.getRedisCredentialsProviderFactory().ifPresent(factory -> {
|
||||
|
||||
redisUri.setCredentialsProvider(factory.createCredentialsProvider(configuration));
|
||||
|
||||
RedisCredentialsProvider sentinelCredentials = factory
|
||||
.createSentinelCredentialsProvider((RedisSentinelConfiguration) configuration);
|
||||
redisUri.getSentinels().forEach(it -> it.setCredentialsProvider(sentinelCredentials));
|
||||
});
|
||||
|
||||
return redisUri;
|
||||
}
|
||||
|
||||
@@ -1267,6 +1277,10 @@ public class LettuceConnectionFactory
|
||||
} else {
|
||||
getRedisPassword().toOptional().ifPresent(builder::withPassword);
|
||||
}
|
||||
|
||||
clientConfiguration.getRedisCredentialsProviderFactory().ifPresent(factory -> {
|
||||
builder.withAuthentication(factory.createCredentialsProvider(configuration));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1456,6 +1470,11 @@ public class LettuceConnectionFactory
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getClientName() {
|
||||
return Optional.ofNullable(clientName);
|
||||
|
||||
@@ -145,6 +145,13 @@ public interface LettucePoolingClientConfiguration extends LettuceClientConfigur
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LettucePoolingClientConfigurationBuilder redisCredentialsProviderFactory(
|
||||
RedisCredentialsProviderFactory redisCredentialsProviderFactory) {
|
||||
super.redisCredentialsProviderFactory(redisCredentialsProviderFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LettucePoolingClientConfigurationBuilder clientName(String clientName) {
|
||||
super.clientName(clientName);
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2022 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 io.lettuce.core.RedisCredentials;
|
||||
import io.lettuce.core.RedisCredentialsProvider;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConfiguration;
|
||||
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Factory interface to create {@link RedisCredentialsProvider} from a {@link RedisConfiguration}. Credentials can be
|
||||
* associated with {@link RedisCredentials#hasUsername() username} and/or {@link RedisCredentials#hasPassword()
|
||||
* password}.
|
||||
* <p>
|
||||
* Credentials are based off the given {@link RedisConfiguration} objects. Changing the credentials in the actual object
|
||||
* affects the constructed {@link RedisCredentials} object. Credentials are requested by the Lettuce client after
|
||||
* connecting to the host. Therefore, credential retrieval is subject to complete within the configured connection
|
||||
* creation timeout to avoid connection failures.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 3.0
|
||||
*/
|
||||
public interface RedisCredentialsProviderFactory {
|
||||
|
||||
/**
|
||||
* Create a {@link RedisCredentialsProvider} for data node authentication given {@link RedisConfiguration}.
|
||||
*
|
||||
* @param redisConfiguration the {@link RedisConfiguration} object.
|
||||
* @return a {@link RedisCredentialsProvider} that emits {@link RedisCredentials} for data node authentication.
|
||||
*/
|
||||
@Nullable
|
||||
default RedisCredentialsProvider createCredentialsProvider(RedisConfiguration redisConfiguration) {
|
||||
|
||||
if (redisConfiguration instanceof RedisConfiguration.WithAuthentication
|
||||
&& ((RedisConfiguration.WithAuthentication) redisConfiguration).getPassword().isPresent()) {
|
||||
|
||||
return RedisCredentialsProvider.from(() -> {
|
||||
|
||||
RedisConfiguration.WithAuthentication withAuthentication = (RedisConfiguration.WithAuthentication) redisConfiguration;
|
||||
|
||||
return RedisCredentials.just(withAuthentication.getUsername(), withAuthentication.getPassword().get());
|
||||
});
|
||||
}
|
||||
|
||||
return () -> Mono.just(AbsentRedisCredentials.ANONYMOUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RedisCredentialsProvider} for Sentinel node authentication given
|
||||
* {@link RedisSentinelConfiguration}.
|
||||
*
|
||||
* @param redisConfiguration the {@link RedisSentinelConfiguration} object.
|
||||
* @return a {@link RedisCredentialsProvider} that emits {@link RedisCredentials} for sentinel authentication.
|
||||
*/
|
||||
default RedisCredentialsProvider createSentinelCredentialsProvider(RedisSentinelConfiguration redisConfiguration) {
|
||||
|
||||
if (redisConfiguration.getSentinelPassword().isPresent()) {
|
||||
|
||||
return RedisCredentialsProvider.from(() -> RedisCredentials.just(redisConfiguration.getSentinelUsername(),
|
||||
redisConfiguration.getSentinelPassword().get()));
|
||||
}
|
||||
|
||||
return () -> Mono.just(AbsentRedisCredentials.ANONYMOUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default anonymous {@link RedisCredentials} without username/password.
|
||||
*/
|
||||
enum AbsentRedisCredentials implements RedisCredentials {
|
||||
|
||||
ANONYMOUS;
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getUsername() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasUsername() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public char[] getPassword() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPassword() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import io.lettuce.core.codec.RedisCodec;
|
||||
import io.lettuce.core.resource.ClientResources;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
@@ -187,6 +188,59 @@ class LettuceConnectionFactoryUnitTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test // GH-2376
|
||||
@SuppressWarnings("unchecked")
|
||||
void credentialsProviderShouldBeSetCorrectlyOnClusterClient() {
|
||||
|
||||
clusterConfig.setUsername("foo");
|
||||
clusterConfig.setPassword("bar");
|
||||
|
||||
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
|
||||
.clientResources(getSharedClientResources())
|
||||
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
|
||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
|
||||
connectionFactory.afterPropertiesSet();
|
||||
ConnectionFactoryTracker.add(connectionFactory);
|
||||
|
||||
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
|
||||
assertThat(client).isInstanceOf(RedisClusterClient.class);
|
||||
|
||||
Iterable<RedisURI> initialUris = (Iterable<RedisURI>) getField(client, "initialUris");
|
||||
|
||||
for (RedisURI uri : initialUris) {
|
||||
|
||||
uri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
assertThat(actual.getUsername()).isEqualTo("foo");
|
||||
assertThat(new String(actual.getPassword())).isEqualTo("bar");
|
||||
}).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test // GH-2376
|
||||
void credentialsProviderShouldBeSetCorrectlyOnStandaloneClient() {
|
||||
|
||||
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("localhost");
|
||||
config.setUsername("foo");
|
||||
config.setPassword("bar");
|
||||
|
||||
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
|
||||
.clientResources(getSharedClientResources())
|
||||
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
|
||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config, clientConfiguration);
|
||||
connectionFactory.afterPropertiesSet();
|
||||
ConnectionFactoryTracker.add(connectionFactory);
|
||||
|
||||
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
|
||||
assertThat(client).isInstanceOf(RedisClient.class);
|
||||
|
||||
RedisURI uri = (RedisURI) getField(client, "redisURI");
|
||||
|
||||
uri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
assertThat(actual.getUsername()).isEqualTo("foo");
|
||||
assertThat(new String(actual.getPassword())).isEqualTo("bar");
|
||||
}).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-524, DATAREDIS-1045, DATAREDIS-1060
|
||||
void passwordShouldNotBeSetOnSentinelClient() {
|
||||
|
||||
@@ -233,6 +287,41 @@ class LettuceConnectionFactoryUnitTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test // GH-2376
|
||||
void sentinelCredentialsProviderShouldBeSetOnSentinelClient() {
|
||||
|
||||
RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections.singleton("host:1234"));
|
||||
config.setUsername("data-user");
|
||||
config.setPassword("data-pwd");
|
||||
config.setSentinelPassword("sentinel-pwd");
|
||||
|
||||
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
|
||||
.clientResources(getSharedClientResources())
|
||||
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
|
||||
|
||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config, clientConfiguration);
|
||||
connectionFactory.afterPropertiesSet();
|
||||
ConnectionFactoryTracker.add(connectionFactory);
|
||||
|
||||
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
|
||||
assertThat(client).isInstanceOf(RedisClient.class);
|
||||
|
||||
RedisURI redisUri = (RedisURI) getField(client, "redisURI");
|
||||
|
||||
redisUri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
assertThat(actual.getUsername()).isEqualTo("data-user");
|
||||
assertThat(new String(actual.getPassword())).isEqualTo("data-pwd");
|
||||
}).verifyComplete();
|
||||
|
||||
for (RedisURI sentinelUri : redisUri.getSentinels()) {
|
||||
|
||||
sentinelUri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
|
||||
assertThat(actual.getUsername()).isNull();
|
||||
assertThat(new String(actual.getPassword())).isEqualTo("sentinel-pwd");
|
||||
}).verifyComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-1060
|
||||
void sentinelPasswordShouldNotLeakIntoDataNodeClient() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user