Skip to content

Commit

Permalink
Introduce apply methods for SSL and authentication settings #1406 #1404
Browse files Browse the repository at this point in the history
RedisURI and its builder now expose methods to apply authentication and SSL settings from a source RedisURI. It is also possible to initialize a RedisURI builder from an existing RedisURI to copy settings.

Previously, several places applied seed/connection settings individually which caused bugs when new properties were introduced. Pulling initialization any apply methods into RedisURI avoids sprawl of code duplicates that evolved differently.
  • Loading branch information
mp911de committed Sep 1, 2020
1 parent 042ce9b commit 969135b
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 47 deletions.
135 changes: 127 additions & 8 deletions src/main/java/io/lettuce/core/RedisURI.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public RedisURI(String host, int port, Duration timeout) {
}

/**
* Returns a new {@link RedisURI.Builder} to construct a {@link RedisURI}.
* Return a new {@link RedisURI.Builder} to construct a {@link RedisURI}.
*
* @return a new {@link RedisURI.Builder} to construct a {@link RedisURI}.
*/
Expand Down Expand Up @@ -285,6 +285,37 @@ public static RedisURI create(URI uri) {
return buildRedisUriFromUri(uri);
}

/**
* Create a new {@link RedisURI.Builder} that is initialized from a plain {@link RedisURI}.
*
* @param source the initialization source, must not be {@code null}.
* @return the initialized builder.
* @since 6.0
*/
public static Builder builder(RedisURI source) {

LettuceAssert.notNull(source, "Source RedisURI must not be null");

Builder builder = builder();
builder.withSsl(source).withAuthentication(source).withTimeout(source.getTimeout()).withDatabase(source.getDatabase());

if (source.getClientName() != null) {
builder.withClientName(source.getClientName());
}

if (source.socket != null) {
builder.socket = source.getSocket();
} else {

if (source.getHost() != null) {
builder.withHost(source.getHost());
builder.withPort(source.getPort());
}
}

return builder;
}

/**
* Returns the host.
*
Expand Down Expand Up @@ -357,6 +388,23 @@ public void setSocket(String socket) {
this.socket = socket;
}

/**
* Apply authentication from another {@link RedisURI}. The authentication settings of the {@code source} URI will be applied
* to this URI. That is in particular username and password. If the source has authentication credentials configured, then
* this URI will use the same credentials. If this URI has authentication configured and the {@code source} URI has no
* authentication, then this URI's authentication credentials will be reset.
*
* @param source must not be {@code null}.
* @since 6.0
*/
public void applyAuthentication(RedisURI source) {

LettuceAssert.notNull(source, "Source RedisURI must not be null");

setUsername(source.getUsername());
setPassword(source.getPassword());
}

/**
* Returns the username.
*
Expand Down Expand Up @@ -412,13 +460,12 @@ public void setPassword(CharSequence password) {
/**
* Sets the password. Use empty char array to skip authentication.
*
* @param password the password, must not be {@code null}.
* @param password the password, can be {@code null}.
* @since 4.4
*/
public void setPassword(char[] password) {

LettuceAssert.notNull(password, "Password must not be null");
this.password = Arrays.copyOf(password, password.length);
this.password = password == null ? null : Arrays.copyOf(password, password.length);
}

/**
Expand Down Expand Up @@ -486,6 +533,22 @@ public void setClientName(String clientName) {
this.clientName = clientName;
}

/**
* Apply authentication from another {@link RedisURI}. The SSL settings of the {@code source} URI will be applied to this
* URI. That is in particular SSL usage, peer verification and StartTLS.
*
* @param source must not be {@code null}.
* @since 6.0
*/
public void applySsl(RedisURI source) {

LettuceAssert.notNull(source, "Source RedisURI must not be null");

setSsl(source.isSsl());
setVerifyPeer(source.isVerifyPeer());
setStartTls(source.isStartTls());
}

/**
* Returns {@code true} if SSL mode is enabled.
*
Expand Down Expand Up @@ -1003,6 +1066,7 @@ private static boolean isSentinel(String scheme) {
return URI_SCHEME_REDIS_SENTINEL.equals(scheme) || URI_SCHEME_REDIS_SENTINEL_SECURE.equals(scheme);
}


/**
* Builder for Redis URI.
*/
Expand Down Expand Up @@ -1141,7 +1205,10 @@ public static Builder sentinel(String host, int port, String masterId) {
* @param masterId sentinel master id
* @param password the Sentinel password (supported since Redis 5.0.1)
* @return new builder with Sentinel host/port.
* @deprecated since 6.0, use {@link #sentinel(String, int, String)} and
* {@link #withAuthentication(String, CharSequence)} instead.
*/
@Deprecated
public static Builder sentinel(String host, int port, String masterId, CharSequence password) {

LettuceAssert.notEmpty(host, "Host must not be empty");
Expand Down Expand Up @@ -1198,7 +1265,7 @@ public Builder withSentinel(String host, int port, CharSequence password) {
RedisURI redisURI = RedisURI.create(host, port);

if (password != null) {
redisURI.setPassword(password.toString());
redisURI.setPassword(password);
}

return withSentinel(redisURI);
Expand Down Expand Up @@ -1249,6 +1316,25 @@ public Builder withPort(int port) {
return this;
}

/**
* Apply authentication from another {@link RedisURI}. The SSL settings of the {@code source} URI will be applied to
* this URI. That is in particular SSL usage, peer verification and StartTLS.
*
* @param source must not be {@code null}.
* @since 6.0
* @return the builder
*/
public Builder withSsl(RedisURI source) {

LettuceAssert.notNull(source, "Source RedisURI must not be null");

withSsl(source.isSsl());
withVerifyPeer(source.isVerifyPeer());
withStartTls(source.isStartTls());

return this;
}

/**
* Adds ssl information to the builder. Sets SSL also for already configured Redis Sentinel nodes.
*
Expand Down Expand Up @@ -1322,6 +1408,7 @@ public Builder withClientName(String clientName) {
* @param username the user name
* @param password the password name
* @return the builder
* @since 6.0
*/
public Builder withAuthentication(String username, CharSequence password) {

Expand All @@ -1332,6 +1419,40 @@ public Builder withAuthentication(String username, CharSequence password) {
return withPassword(password);
}

/**
* Apply authentication from another {@link RedisURI}. The authentication settings of the {@code source} URI will be
* applied to this builder.
*
* @param source must not be {@code null}.
* @since 6.0
*/
public Builder withAuthentication(RedisURI source) {

LettuceAssert.notNull(source, "Source RedisURI must not be null");

this.username = source.getUsername();
withPassword(source.getPassword());

return this;
}

/**
* Configures authentication.
*
* @param username the user name
* @param password the password name
* @return the builder
* @since 6.0
*/
public Builder withAuthentication(String username, char[] password) {

LettuceAssert.notNull(username, "User name must not be null");
LettuceAssert.notNull(password, "Password must not be null");

this.username = username;
return withPassword(password);
}

/**
* Configures authentication.
*
Expand Down Expand Up @@ -1375,9 +1496,7 @@ public Builder withPassword(CharSequence password) {
*/
public Builder withPassword(char[] password) {

LettuceAssert.notNull(password, "Password must not be null");

this.password = Arrays.copyOf(password, password.length);
this.password = password == null ? null : Arrays.copyOf(password, password.length);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,9 @@ public static List<RedisURI> toRedisURIs(URI uri) {
*/
static void applyUriConnectionSettings(RedisURI from, RedisURI to) {

if (from.getPassword() != null && from.getPassword().length != 0) {
to.setPassword(new String(from.getPassword()));
}

to.applyAuthentication(from);
to.applySsl(from);
to.setTimeout(from.getTimeout());
to.setSsl(from.isSsl());
to.setStartTls(from.isStartTls());
to.setVerifyPeer(from.isVerifyPeer());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,7 @@ class RedisUpstreamReplicaNode implements RedisNodeDescription {

RedisUpstreamReplicaNode(String host, int port, RedisURI seed, Role role) {

RedisURI.Builder builder = RedisURI.Builder.redis(host, port).withSsl(seed.isSsl()).withVerifyPeer(seed.isVerifyPeer())
.withStartTls(seed.isStartTls());
if (seed.getPassword() != null && seed.getPassword().length != 0) {
builder.withPassword(seed.getPassword());
}

if (seed.getClientName() != null) {
builder.withClientName(seed.getClientName());
}

builder.withDatabase(seed.getDatabase());

this.redisURI = builder.build();
this.redisURI = RedisURI.builder(seed).withHost(host).withPort(port).build();
this.role = role;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,17 +322,7 @@ class DefaultConnectionFactory implements Function<ConnectionKey, CompletionStag
@Override
public ConnectionFuture<StatefulRedisConnection<K, V>> apply(ConnectionKey key) {

RedisURI.Builder builder = RedisURI.Builder.redis(key.host, key.port).withSsl(initialRedisUri.isSsl())
.withVerifyPeer(initialRedisUri.isVerifyPeer()).withStartTls(initialRedisUri.isStartTls());

if (initialRedisUri.getPassword() != null && initialRedisUri.getPassword().length != 0) {
builder.withPassword(initialRedisUri.getPassword());
}

if (initialRedisUri.getClientName() != null) {
builder.withClientName(initialRedisUri.getClientName());
}
builder.withDatabase(initialRedisUri.getDatabase());
RedisURI.Builder builder = RedisURI.builder(initialRedisUri).withHost(key.host).withPort(key.port);

ConnectionFuture<StatefulRedisConnection<K, V>> connectionFuture = redisClient.connectAsync(redisCodec,
builder.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class UpstreamReplicaTopologyRefresh {

private final TopologyProvider topologyProvider;

private ScheduledExecutorService eventExecutors;
private final ScheduledExecutorService eventExecutors;

UpstreamReplicaTopologyRefresh(RedisClient client, TopologyProvider topologyProvider) {
this(new RedisClientNodeConnectionFactory(client), client.getResources().eventExecutorGroup(), topologyProvider);
Expand All @@ -73,7 +73,7 @@ public Mono<List<RedisNodeDescription>> getNodes(RedisURI seed) {
CompletableFuture<List<RedisNodeDescription>> future = topologyProvider.getNodesAsync();

Mono<List<RedisNodeDescription>> initialNodes = Mono.fromFuture(future).doOnNext(nodes -> {
addPasswordIfNeeded(nodes, seed);
applyAuthenticationCredentials(nodes, seed);
});

return initialNodes.map(this::getConnections)
Expand Down Expand Up @@ -137,12 +137,10 @@ private AsyncConnections getConnections(Iterable<RedisNodeDescription> nodes) {
return connections;
}

private static void addPasswordIfNeeded(List<RedisNodeDescription> nodes, RedisURI seed) {
private static void applyAuthenticationCredentials(List<RedisNodeDescription> nodes, RedisURI seed) {

if (seed.getPassword() != null && seed.getPassword().length != 0) {
for (RedisNodeDescription node : nodes) {
node.getUri().setPassword(new String(seed.getPassword()));
}
for (RedisNodeDescription node : nodes) {
node.getUri().applyAuthentication(seed);
}
}

Expand Down
72 changes: 71 additions & 1 deletion src/test/java/io/lettuce/core/RedisURIBuilderUnitTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

/**
Expand Down Expand Up @@ -275,4 +274,75 @@ void redisSocketWithPassword() throws IOException {
assertThat(result.getPort()).isEqualTo(RedisURI.DEFAULT_REDIS_PORT);
assertThat(result.isSsl()).isFalse();
}

@Test
void shouldApplySslSettings() {

RedisURI source = new RedisURI();
source.setSsl(true);
source.setVerifyPeer(false);
source.setStartTls(true);

RedisURI target = RedisURI.builder().withHost("localhost").withSsl(source).build();

assertThat(target.isSsl()).isTrue();
assertThat(target.isVerifyPeer()).isFalse();
assertThat(target.isStartTls()).isTrue();
}

@Test
void shouldApplyAuthentication() {

RedisURI source = new RedisURI();
source.setUsername("foo");
source.setPassword("bar");

RedisURI target = RedisURI.builder().withHost("localhost").withAuthentication(source).build();

assertThat(target.getUsername()).isEqualTo("foo");
assertThat(target.getPassword()).isEqualTo("bar".toCharArray());
}

@Test
void shouldInitializeBuilder() {

RedisURI source = new RedisURI();
source.setHost("localhost");
source.setPort(1234);
source.setTimeout(Duration.ofSeconds(2));
source.setClientName("foo");
source.setUsername("foo");
source.setPassword("bar");
source.setDatabase(4);
source.setSsl(true);
source.setVerifyPeer(false);
source.setStartTls(true);

RedisURI target = RedisURI.builder(source).build();

source.setPassword("baz");

assertThat(target.getHost()).isEqualTo(source.getHost());
assertThat(target.getPort()).isEqualTo(source.getPort());
assertThat(target.getUsername()).isEqualTo(source.getUsername());
assertThat(target.getPassword()).isEqualTo("bar".toCharArray());
assertThat(target.getTimeout()).isEqualTo(source.getTimeout());
assertThat(target.getClientName()).isEqualTo(source.getClientName());
assertThat(target.getSocket()).isEqualTo(source.getSocket());
assertThat(target.getDatabase()).isEqualTo(source.getDatabase());
assertThat(target.isStartTls()).isEqualTo(source.isStartTls());
assertThat(target.isSsl()).isEqualTo(source.isSsl());
assertThat(target.isVerifyPeer()).isEqualTo(source.isVerifyPeer());
}

@Test
void shouldInitializeBuilderUsingSocket() {

RedisURI source = new RedisURI();
source.setSocket("localhost");

RedisURI target = RedisURI.builder(source).build();

assertThat(target.getSocket()).isEqualTo(source.getSocket());
}
}
Loading

0 comments on commit 969135b

Please sign in to comment.