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:
Mark Paluch
2022-08-24 10:34:52 +02:00
committed by Christoph Strobl
parent c3ee0d8b3c
commit 8f37abe950
7 changed files with 267 additions and 3 deletions

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}
}