Skip to content

Commit 8f37abe

Browse files
mp911dechristophstrobl
authored andcommitted
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
1 parent c3ee0d8 commit 8f37abe

File tree

7 files changed

+267
-3
lines changed

7 files changed

+267
-3
lines changed

src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettuceClientConfiguration.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration {
4141
private final Optional<ClientOptions> clientOptions;
4242
private final Optional<String> clientName;
4343
private final Optional<ReadFrom> readFrom;
44+
private final Optional<RedisCredentialsProviderFactory> redisCredentialsProviderFactory;
4445
private final Duration timeout;
4546
private final Duration shutdownTimeout;
4647
private final Duration shutdownQuietPeriod;
4748

4849
DefaultLettuceClientConfiguration(boolean useSsl, boolean verifyPeer, boolean startTls,
4950
@Nullable ClientResources clientResources, @Nullable ClientOptions clientOptions, @Nullable String clientName,
50-
@Nullable ReadFrom readFrom, Duration timeout, Duration shutdownTimeout, @Nullable Duration shutdownQuietPeriod) {
51+
@Nullable ReadFrom readFrom, @Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory,
52+
Duration timeout, Duration shutdownTimeout, @Nullable Duration shutdownQuietPeriod) {
5153

5254
this.useSsl = useSsl;
5355
this.verifyPeer = verifyPeer;
@@ -56,6 +58,7 @@ class DefaultLettuceClientConfiguration implements LettuceClientConfiguration {
5658
this.clientOptions = Optional.ofNullable(clientOptions);
5759
this.clientName = Optional.ofNullable(clientName);
5860
this.readFrom = Optional.ofNullable(readFrom);
61+
this.redisCredentialsProviderFactory = Optional.ofNullable(redisCredentialsProviderFactory);
5962
this.timeout = timeout;
6063
this.shutdownTimeout = shutdownTimeout;
6164
this.shutdownQuietPeriod = shutdownQuietPeriod != null ? shutdownQuietPeriod : shutdownTimeout;
@@ -96,6 +99,11 @@ public Optional<ReadFrom> getReadFrom() {
9699
return readFrom;
97100
}
98101

102+
@Override
103+
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
104+
return redisCredentialsProviderFactory;
105+
}
106+
99107
@Override
100108
public Duration getCommandTimeout() {
101109
return timeout;

src/main/java/org/springframework/data/redis/connection/lettuce/DefaultLettucePoolingClientConfiguration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public Optional<ReadFrom> getReadFrom() {
7979
return clientConfiguration.getReadFrom();
8080
}
8181

82+
@Override
83+
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
84+
return clientConfiguration.getRedisCredentialsProviderFactory();
85+
}
86+
8287
@Override
8388
public Duration getCommandTimeout() {
8489
return clientConfiguration.getCommandTimeout();

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceClientConfiguration.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ public interface LettuceClientConfiguration {
9494
*/
9595
Optional<ReadFrom> getReadFrom();
9696

97+
/**
98+
* @return the optional {@link RedisCredentialsProviderFactory}.
99+
* @since 3.0
100+
*/
101+
Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory();
102+
97103
/**
98104
* @return the timeout.
99105
*/
@@ -166,6 +172,7 @@ class LettuceClientConfigurationBuilder {
166172
ClientOptions clientOptions = ClientOptions.builder().timeoutOptions(TimeoutOptions.enabled()).build();
167173
@Nullable String clientName;
168174
@Nullable ReadFrom readFrom;
175+
@Nullable RedisCredentialsProviderFactory redisCredentialsProviderFactory;
169176
Duration timeout = Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT);
170177
Duration shutdownTimeout = Duration.ofMillis(100);
171178
@Nullable Duration shutdownQuietPeriod;
@@ -242,7 +249,7 @@ public LettuceClientConfigurationBuilder clientOptions(ClientOptions clientOptio
242249
*
243250
* @param readFrom must not be {@literal null}.
244251
* @return {@literal this} builder.
245-
* @throws IllegalArgumentException if clientOptions is {@literal null}.
252+
* @throws IllegalArgumentException if readFrom is {@literal null}.
246253
* @since 2.1
247254
*/
248255
public LettuceClientConfigurationBuilder readFrom(ReadFrom readFrom) {
@@ -253,6 +260,24 @@ public LettuceClientConfigurationBuilder readFrom(ReadFrom readFrom) {
253260
return this;
254261
}
255262

263+
/**
264+
* Configure a {@link RedisCredentialsProviderFactory} to obtain {@link io.lettuce.core.RedisCredentialsProvider}
265+
* instances to support credential rotation.
266+
*
267+
* @param redisCredentialsProviderFactory must not be {@literal null}.
268+
* @return {@literal this} builder.
269+
* @throws IllegalArgumentException if redisCredentialsProviderFactory is {@literal null}.
270+
* @since 3.0
271+
*/
272+
public LettuceClientConfigurationBuilder redisCredentialsProviderFactory(
273+
RedisCredentialsProviderFactory redisCredentialsProviderFactory) {
274+
275+
Assert.notNull(redisCredentialsProviderFactory, "RedisCredentialsProviderFactory must not be null");
276+
277+
this.redisCredentialsProviderFactory = redisCredentialsProviderFactory;
278+
return this;
279+
}
280+
256281
/**
257282
* Configure a {@code clientName} to be set with {@code CLIENT SETNAME}.
258283
*
@@ -323,7 +348,7 @@ public LettuceClientConfigurationBuilder shutdownQuietPeriod(Duration shutdownQu
323348
public LettuceClientConfiguration build() {
324349

325350
return new DefaultLettuceClientConfiguration(useSsl, verifyPeer, startTls, clientResources, clientOptions,
326-
clientName, readFrom, timeout, shutdownTimeout, shutdownQuietPeriod);
351+
clientName, readFrom, redisCredentialsProviderFactory, timeout, shutdownTimeout, shutdownQuietPeriod);
327352
}
328353
}
329354

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.lettuce.core.ReadFrom;
2323
import io.lettuce.core.RedisClient;
2424
import io.lettuce.core.RedisConnectionException;
25+
import io.lettuce.core.RedisCredentialsProvider;
2526
import io.lettuce.core.RedisURI;
2627
import io.lettuce.core.api.StatefulConnection;
2728
import io.lettuce.core.api.StatefulRedisConnection;
@@ -1216,6 +1217,15 @@ private RedisURI getSentinelRedisURI() {
12161217

12171218
redisUri.setDatabase(getDatabase());
12181219

1220+
clientConfiguration.getRedisCredentialsProviderFactory().ifPresent(factory -> {
1221+
1222+
redisUri.setCredentialsProvider(factory.createCredentialsProvider(configuration));
1223+
1224+
RedisCredentialsProvider sentinelCredentials = factory
1225+
.createSentinelCredentialsProvider((RedisSentinelConfiguration) configuration);
1226+
redisUri.getSentinels().forEach(it -> it.setCredentialsProvider(sentinelCredentials));
1227+
});
1228+
12191229
return redisUri;
12201230
}
12211231

@@ -1267,6 +1277,10 @@ private void applyAuthentication(RedisURI.Builder builder) {
12671277
} else {
12681278
getRedisPassword().toOptional().ifPresent(builder::withPassword);
12691279
}
1280+
1281+
clientConfiguration.getRedisCredentialsProviderFactory().ifPresent(factory -> {
1282+
builder.withAuthentication(factory.createCredentialsProvider(configuration));
1283+
});
12701284
}
12711285

12721286
@Override
@@ -1456,6 +1470,11 @@ public Optional<ReadFrom> getReadFrom() {
14561470
return Optional.empty();
14571471
}
14581472

1473+
@Override
1474+
public Optional<RedisCredentialsProviderFactory> getRedisCredentialsProviderFactory() {
1475+
return Optional.empty();
1476+
}
1477+
14591478
@Override
14601479
public Optional<String> getClientName() {
14611480
return Optional.ofNullable(clientName);

src/main/java/org/springframework/data/redis/connection/lettuce/LettucePoolingClientConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ public LettucePoolingClientConfigurationBuilder readFrom(ReadFrom readFrom) {
145145
return this;
146146
}
147147

148+
@Override
149+
public LettucePoolingClientConfigurationBuilder redisCredentialsProviderFactory(
150+
RedisCredentialsProviderFactory redisCredentialsProviderFactory) {
151+
super.redisCredentialsProviderFactory(redisCredentialsProviderFactory);
152+
return this;
153+
}
154+
148155
@Override
149156
public LettucePoolingClientConfigurationBuilder clientName(String clientName) {
150157
super.clientName(clientName);
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.connection.lettuce;
17+
18+
import io.lettuce.core.RedisCredentials;
19+
import io.lettuce.core.RedisCredentialsProvider;
20+
import reactor.core.publisher.Mono;
21+
22+
import org.springframework.data.redis.connection.RedisConfiguration;
23+
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* Factory interface to create {@link RedisCredentialsProvider} from a {@link RedisConfiguration}. Credentials can be
28+
* associated with {@link RedisCredentials#hasUsername() username} and/or {@link RedisCredentials#hasPassword()
29+
* password}.
30+
* <p>
31+
* Credentials are based off the given {@link RedisConfiguration} objects. Changing the credentials in the actual object
32+
* affects the constructed {@link RedisCredentials} object. Credentials are requested by the Lettuce client after
33+
* connecting to the host. Therefore, credential retrieval is subject to complete within the configured connection
34+
* creation timeout to avoid connection failures.
35+
*
36+
* @author Mark Paluch
37+
* @since 3.0
38+
*/
39+
public interface RedisCredentialsProviderFactory {
40+
41+
/**
42+
* Create a {@link RedisCredentialsProvider} for data node authentication given {@link RedisConfiguration}.
43+
*
44+
* @param redisConfiguration the {@link RedisConfiguration} object.
45+
* @return a {@link RedisCredentialsProvider} that emits {@link RedisCredentials} for data node authentication.
46+
*/
47+
@Nullable
48+
default RedisCredentialsProvider createCredentialsProvider(RedisConfiguration redisConfiguration) {
49+
50+
if (redisConfiguration instanceof RedisConfiguration.WithAuthentication
51+
&& ((RedisConfiguration.WithAuthentication) redisConfiguration).getPassword().isPresent()) {
52+
53+
return RedisCredentialsProvider.from(() -> {
54+
55+
RedisConfiguration.WithAuthentication withAuthentication = (RedisConfiguration.WithAuthentication) redisConfiguration;
56+
57+
return RedisCredentials.just(withAuthentication.getUsername(), withAuthentication.getPassword().get());
58+
});
59+
}
60+
61+
return () -> Mono.just(AbsentRedisCredentials.ANONYMOUS);
62+
}
63+
64+
/**
65+
* Create a {@link RedisCredentialsProvider} for Sentinel node authentication given
66+
* {@link RedisSentinelConfiguration}.
67+
*
68+
* @param redisConfiguration the {@link RedisSentinelConfiguration} object.
69+
* @return a {@link RedisCredentialsProvider} that emits {@link RedisCredentials} for sentinel authentication.
70+
*/
71+
default RedisCredentialsProvider createSentinelCredentialsProvider(RedisSentinelConfiguration redisConfiguration) {
72+
73+
if (redisConfiguration.getSentinelPassword().isPresent()) {
74+
75+
return RedisCredentialsProvider.from(() -> RedisCredentials.just(redisConfiguration.getSentinelUsername(),
76+
redisConfiguration.getSentinelPassword().get()));
77+
}
78+
79+
return () -> Mono.just(AbsentRedisCredentials.ANONYMOUS);
80+
}
81+
82+
/**
83+
* Default anonymous {@link RedisCredentials} without username/password.
84+
*/
85+
enum AbsentRedisCredentials implements RedisCredentials {
86+
87+
ANONYMOUS;
88+
89+
@Override
90+
@Nullable
91+
public String getUsername() {
92+
return null;
93+
}
94+
95+
@Override
96+
public boolean hasUsername() {
97+
return false;
98+
}
99+
100+
@Override
101+
@Nullable
102+
public char[] getPassword() {
103+
return null;
104+
}
105+
106+
@Override
107+
public boolean hasPassword() {
108+
return false;
109+
}
110+
}
111+
}

src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.lettuce.core.resource.ClientResources;
3838
import lombok.AllArgsConstructor;
3939
import lombok.Data;
40+
import reactor.test.StepVerifier;
4041

4142
import java.time.Duration;
4243
import java.util.Collections;
@@ -187,6 +188,59 @@ void passwordShouldBeSetCorrectlyOnClusterClient() {
187188
}
188189
}
189190

191+
@Test // GH-2376
192+
@SuppressWarnings("unchecked")
193+
void credentialsProviderShouldBeSetCorrectlyOnClusterClient() {
194+
195+
clusterConfig.setUsername("foo");
196+
clusterConfig.setPassword("bar");
197+
198+
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
199+
.clientResources(getSharedClientResources())
200+
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
201+
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
202+
connectionFactory.afterPropertiesSet();
203+
ConnectionFactoryTracker.add(connectionFactory);
204+
205+
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
206+
assertThat(client).isInstanceOf(RedisClusterClient.class);
207+
208+
Iterable<RedisURI> initialUris = (Iterable<RedisURI>) getField(client, "initialUris");
209+
210+
for (RedisURI uri : initialUris) {
211+
212+
uri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
213+
assertThat(actual.getUsername()).isEqualTo("foo");
214+
assertThat(new String(actual.getPassword())).isEqualTo("bar");
215+
}).verifyComplete();
216+
}
217+
}
218+
219+
@Test // GH-2376
220+
void credentialsProviderShouldBeSetCorrectlyOnStandaloneClient() {
221+
222+
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("localhost");
223+
config.setUsername("foo");
224+
config.setPassword("bar");
225+
226+
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
227+
.clientResources(getSharedClientResources())
228+
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
229+
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config, clientConfiguration);
230+
connectionFactory.afterPropertiesSet();
231+
ConnectionFactoryTracker.add(connectionFactory);
232+
233+
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
234+
assertThat(client).isInstanceOf(RedisClient.class);
235+
236+
RedisURI uri = (RedisURI) getField(client, "redisURI");
237+
238+
uri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
239+
assertThat(actual.getUsername()).isEqualTo("foo");
240+
assertThat(new String(actual.getPassword())).isEqualTo("bar");
241+
}).verifyComplete();
242+
}
243+
190244
@Test // DATAREDIS-524, DATAREDIS-1045, DATAREDIS-1060
191245
void passwordShouldNotBeSetOnSentinelClient() {
192246

@@ -233,6 +287,41 @@ void sentinelPasswordShouldBeSetOnSentinelClient() {
233287
}
234288
}
235289

290+
@Test // GH-2376
291+
void sentinelCredentialsProviderShouldBeSetOnSentinelClient() {
292+
293+
RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections.singleton("host:1234"));
294+
config.setUsername("data-user");
295+
config.setPassword("data-pwd");
296+
config.setSentinelPassword("sentinel-pwd");
297+
298+
LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
299+
.clientResources(getSharedClientResources())
300+
.redisCredentialsProviderFactory(new RedisCredentialsProviderFactory() {}).build();
301+
302+
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config, clientConfiguration);
303+
connectionFactory.afterPropertiesSet();
304+
ConnectionFactoryTracker.add(connectionFactory);
305+
306+
AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
307+
assertThat(client).isInstanceOf(RedisClient.class);
308+
309+
RedisURI redisUri = (RedisURI) getField(client, "redisURI");
310+
311+
redisUri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
312+
assertThat(actual.getUsername()).isEqualTo("data-user");
313+
assertThat(new String(actual.getPassword())).isEqualTo("data-pwd");
314+
}).verifyComplete();
315+
316+
for (RedisURI sentinelUri : redisUri.getSentinels()) {
317+
318+
sentinelUri.getCredentialsProvider().resolveCredentials().as(StepVerifier::create).consumeNextWith(actual -> {
319+
assertThat(actual.getUsername()).isNull();
320+
assertThat(new String(actual.getPassword())).isEqualTo("sentinel-pwd");
321+
}).verifyComplete();
322+
}
323+
}
324+
236325
@Test // DATAREDIS-1060
237326
void sentinelPasswordShouldNotLeakIntoDataNodeClient() {
238327

0 commit comments

Comments
 (0)