DATAREDIS-1046 - Use ACL authentication when username is configured.

Upgrade to Redis 6 for tests. Introduce support for username in configurations that support authentication.

Original Pull Request: #558
This commit is contained in:
Mark Paluch
2020-09-01 15:04:33 +02:00
committed by Christoph Strobl
parent da5bab1fb2
commit d97c5df130
16 changed files with 437 additions and 51 deletions

View File

@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.core.env.MapPropertySource;
@@ -49,6 +50,7 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon
private Set<RedisNode> clusterNodes;
private @Nullable Integer maxRedirects;
private Optional<String> username = Optional.empty();
private RedisPassword password = RedisPassword.none();
/**
@@ -182,6 +184,24 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String)
*/
@Override
public void setUsername(String username) {
this.username = Optional.of(username);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername()
*/
@Override
public Optional<String> getUsername() {
return this.username;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword()

View File

@@ -17,6 +17,7 @@ package org.springframework.data.redis.connection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
@@ -51,11 +52,11 @@ public interface RedisConfiguration {
/**
* Get the configured {@link RedisPassword} if the current {@link RedisConfiguration} is
* {@link #isPasswordAware(RedisConfiguration) password aware} or evaluate and return the value of the given
* {@link #isAuthenticationAware(RedisConfiguration) password aware} or evaluate and return the value of the given
* {@link Supplier}.
*
* @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not
* {@link #isPasswordAware(RedisConfiguration) password aware}.
* {@link #isAuthenticationAware(RedisConfiguration) password aware}.
* @return never {@literal null}.
* @throws IllegalArgumentException if {@code other} is {@literal null}.
*/
@@ -67,8 +68,8 @@ public interface RedisConfiguration {
* @param configuration can be {@literal null}.
* @return {@code true} if given {@link RedisConfiguration} is instance of {@link WithPassword}.
*/
static boolean isPasswordAware(@Nullable RedisConfiguration configuration) {
return configuration instanceof WithPassword;
static boolean isAuthenticationAware(@Nullable RedisConfiguration configuration) {
return configuration instanceof WithAuthentication;
}
/**
@@ -136,14 +137,28 @@ public interface RedisConfiguration {
/**
* @param configuration can be {@literal null}.
* @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not
* {@link #isPasswordAware(RedisConfiguration) password aware}.
* {@link #isAuthenticationAware(RedisConfiguration) password aware}.
* @return never {@literal null}.
* @throws IllegalArgumentException if {@code other} is {@literal null}.
*/
static Optional<String> getUsernameOrElse(@Nullable RedisConfiguration configuration,
Supplier<Optional<String>> other) {
Assert.notNull(other, "Other must not be null!");
return isAuthenticationAware(configuration) ? ((WithAuthentication) configuration).getUsername() : other.get();
}
/**
* @param configuration can be {@literal null}.
* @param other a {@code Supplier} whose result is returned if given {@link RedisConfiguration} is not
* {@link #isAuthenticationAware(RedisConfiguration) password aware}.
* @return never {@literal null}.
* @throws IllegalArgumentException if {@code other} is {@literal null}.
*/
static RedisPassword getPasswordOrElse(@Nullable RedisConfiguration configuration, Supplier<RedisPassword> other) {
Assert.notNull(other, "Other must not be null!");
return isPasswordAware(configuration) ? ((WithPassword) configuration).getPassword() : other.get();
return isAuthenticationAware(configuration) ? ((WithAuthentication) configuration).getPassword() : other.get();
}
/**
@@ -178,9 +193,17 @@ public interface RedisConfiguration {
* {@link RedisConfiguration} part suitable for configurations that may use authentication when connecting.
*
* @author Christoph Strobl
* @since 2.1
* @author Mark Paluch
* @since 2.4
*/
interface WithPassword {
interface WithAuthentication {
/**
* Create and set a username with the given {@link String}. Requires Redis 6 or newer.
*
* @param username the username.
*/
void setUsername(String username);
/**
* Create and set a {@link RedisPassword} for given {@link String}.
@@ -207,6 +230,13 @@ public interface RedisConfiguration {
*/
void setPassword(RedisPassword password);
/**
* Get the username to use when connecting.
*
* @return {@link Optional#empty()} if none set.
*/
Optional<String> getUsername();
/**
* Get the RedisPassword to use when connecting.
*
@@ -215,6 +245,16 @@ public interface RedisConfiguration {
RedisPassword getPassword();
}
/**
* {@link RedisConfiguration} part suitable for configurations that may use authentication when connecting.
*
* @author Christoph Strobl
* @since 2.1
*/
interface WithPassword extends WithAuthentication {
}
/**
* {@link RedisConfiguration} part suitable for configurations that use a specific database.
*
@@ -339,7 +379,17 @@ public interface RedisConfiguration {
Set<RedisNode> getSentinels();
/**
* Get the {@link RedisPassword} used when authenticating with a Redis Server..
* Get the username used when authenticating with a Redis Server.
*
* @return never {@literal null}.
* @since 2.4
*/
default Optional<String> getDataNodeUsername() {
return getUsername();
}
/**
* Get the {@link RedisPassword} used when authenticating with a Redis Server.
*
* @return never {@literal null}.
* @since 2.2.2

View File

@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.springframework.core.env.MapPropertySource;
@@ -50,6 +51,7 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC
private Set<RedisNode> sentinels;
private int database;
private Optional<String> dataNodeUsername = Optional.empty();
private RedisPassword dataNodePassword = RedisPassword.none();
private RedisPassword sentinelPassword = RedisPassword.none();
@@ -229,6 +231,24 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC
this.database = index;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String)
*/
@Override
public void setUsername(String username) {
this.dataNodeUsername = Optional.of(username);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername()
*/
@Override
public Optional<String> getUsername() {
return this.dataNodeUsername;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.redis.connection;
import java.util.Optional;
import org.springframework.data.redis.connection.RedisConfiguration.DomainSocketConfiguration;
import org.springframework.util.Assert;
@@ -32,6 +34,7 @@ public class RedisSocketConfiguration implements RedisConfiguration, DomainSocke
private String socket = DEFAULT_SOCKET;
private int database;
private Optional<String> username = Optional.empty();
private RedisPassword password = RedisPassword.none();
/**
@@ -92,6 +95,24 @@ public class RedisSocketConfiguration implements RedisConfiguration, DomainSocke
this.database = index;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String)
*/
@Override
public void setUsername(String username) {
this.username = Optional.of(username);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername()
*/
@Override
public Optional<String> getUsername() {
return this.username;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword()

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.redis.connection;
import java.util.Optional;
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
import org.springframework.data.redis.connection.RedisConfiguration.WithHostAndPort;
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
@@ -37,6 +39,7 @@ public class RedisStandaloneConfiguration
private String hostName = DEFAULT_HOST;
private int port = DEFAULT_PORT;
private int database;
private Optional<String> username = Optional.empty();
private RedisPassword password = RedisPassword.none();
/**
@@ -125,6 +128,24 @@ public class RedisStandaloneConfiguration
this.database = index;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String)
*/
@Override
public void setUsername(String username) {
this.username = Optional.of(username);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername()
*/
@Override
public Optional<String> getUsername() {
return this.username;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword()

View File

@@ -18,6 +18,7 @@ package org.springframework.data.redis.connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.springframework.data.redis.connection.RedisConfiguration.StaticMasterReplicaConfiguration;
import org.springframework.util.Assert;
@@ -40,6 +41,7 @@ public class RedisStaticMasterReplicaConfiguration implements RedisConfiguration
private List<RedisStandaloneConfiguration> nodes = new ArrayList<>();
private int database;
private Optional<String> username = Optional.empty();
private RedisPassword password = RedisPassword.none();
/**
@@ -130,6 +132,24 @@ public class RedisStaticMasterReplicaConfiguration implements RedisConfiguration
this.nodes.forEach(it -> it.setDatabase(database));
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#setUsername(String)
*/
@Override
public void setUsername(String username) {
this.username = Optional.of(username);
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithAuthentication#getUsername()
*/
@Override
public Optional<String> getUsername() {
return this.username;
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.connection.RedisConfiguration.WithPassword#getPassword()

View File

@@ -328,6 +328,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
clientConfiguration.getHostnameVerifier().orElse(null));
getRedisPassword().map(String::new).ifPresent(shardInfo::setPassword);
getRedisUsername().ifPresent(shardInfo::setUser);
int readTimeout = getReadTimeout();
@@ -369,9 +370,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
*/
protected Pool<Jedis> createRedisSentinelPool(RedisSentinelConfiguration config) {
GenericObjectPoolConfig poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig();
GenericObjectPoolConfig<?> poolConfig = getPoolConfig() != null ? getPoolConfig() : new JedisPoolConfig();
return new JedisSentinelPool(config.getMaster().getName(), convertToJedisSentinelSet(config.getSentinels()),
poolConfig, getConnectTimeout(), getReadTimeout(), getPassword(), getDatabase(), getClientName());
poolConfig, getConnectTimeout(), getReadTimeout(), getUsername(), getPassword(), getDatabase(),
getClientName());
}
/**
@@ -383,7 +386,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
protected Pool<Jedis> createRedisPool() {
return new JedisPool(getPoolConfig(), getHostName(), getPort(), getConnectTimeout(), getReadTimeout(),
getPassword(), getDatabase(), getClientName(), isUseSsl(),
getUsername(), getPassword(), getDatabase(), getClientName(), isUseSsl(),
clientConfiguration.getSslSocketFactory().orElse(null), //
clientConfiguration.getSslParameters().orElse(null), //
clientConfiguration.getHostnameVerifier().orElse(null));
@@ -414,7 +417,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
* @return the actual {@link JedisCluster}.
* @since 1.7
*/
protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, GenericObjectPoolConfig poolConfig) {
protected JedisCluster createCluster(RedisClusterConfiguration clusterConfig, GenericObjectPoolConfig<?> poolConfig) {
Assert.notNull(clusterConfig, "Cluster configuration must not be null!");
@@ -425,7 +428,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
int redirects = clusterConfig.getMaxRedirects() != null ? clusterConfig.getMaxRedirects() : 5;
return new JedisCluster(hostAndPort, getConnectTimeout(), getReadTimeout(), redirects, getPassword(),
return new JedisCluster(hostAndPort, getConnectTimeout(), getReadTimeout(), redirects, getUsername(), getPassword(),
getClientName(), poolConfig, isUseSsl(), clientConfiguration.getSslSocketFactory().orElse(null),
clientConfiguration.getSslParameters().orElse(null), clientConfiguration.getHostnameVerifier().orElse(null),
null);
@@ -544,6 +547,16 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
getMutableConfiguration().setUseSsl(useSsl);
}
/**
* Returns the username used for authenticating with the Redis server.
*
* @return username for authentication.
*/
@Nullable
private String getUsername() {
return getRedisUsername().orElse(null);
}
/**
* Returns the password used for authenticating with the Redis server.
*
@@ -554,6 +567,10 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
return getRedisPassword().map(String::new).orElse(null);
}
private Optional<String> getRedisUsername() {
return RedisConfiguration.getUsernameOrElse(this.configuration, standaloneConfig::getUsername);
}
private RedisPassword getRedisPassword() {
return RedisConfiguration.getPasswordOrElse(this.configuration, standaloneConfig::getPassword);
}
@@ -568,7 +585,7 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean,
@Deprecated
public void setPassword(String password) {
if (RedisConfiguration.isPasswordAware(configuration)) {
if (RedisConfiguration.isAuthenticationAware(configuration)) {
((WithPassword) configuration).setPassword(password);
return;

View File

@@ -788,6 +788,9 @@ public class LettuceConnectionFactory
this.getMutableConfiguration().setClientName(clientName);
}
private Optional<String> getRedisUsername() {
return RedisConfiguration.getUsernameOrElse(configuration, standaloneConfig::getUsername);
}
/**
* Returns the password used for authenticating with the Redis server.
*
@@ -812,7 +815,7 @@ public class LettuceConnectionFactory
@Deprecated
public void setPassword(String password) {
if (RedisConfiguration.isPasswordAware(configuration)) {
if (RedisConfiguration.isAuthenticationAware(configuration)) {
((WithPassword) configuration).setPassword(password);
return;
@@ -1135,7 +1138,8 @@ public class LettuceConnectionFactory
RedisURI.Builder builder = RedisURI.Builder.redis(host, port);
getRedisPassword().toOptional().ifPresent(builder::withPassword);
applyAuthentication(builder);
clientConfiguration.getClientName().ifPresent(builder::withClientName);
builder.withDatabase(getDatabase());
@@ -1151,13 +1155,25 @@ public class LettuceConnectionFactory
RedisURI.Builder builder = RedisURI.Builder.socket(socketPath);
getRedisPassword().toOptional().ifPresent(builder::withPassword);
applyAuthentication(builder);
builder.withDatabase(getDatabase());
builder.withTimeout(clientConfiguration.getCommandTimeout());
return builder.build();
}
private void applyAuthentication(RedisURI.Builder builder) {
Optional<String> username = getRedisUsername();
if (username.isPresent()) {
// See https://github.com/lettuce-io/lettuce-core/issues/1404
username.ifPresent(
it -> builder.withAuthentication(it, new String(getRedisPassword().toOptional().orElse(new char[0]))));
} else {
getRedisPassword().toOptional().ifPresent(builder::withPassword);
}
}
@Override
public RedisSentinelConnection getSentinelConnection() {
return new LettuceSentinelConnection(connectionProvider);

View File

@@ -18,16 +18,11 @@ package org.springframework.data.redis.connection.lettuce;
import io.lettuce.core.*;
import io.lettuce.core.cluster.models.partitions.Partitions;
import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag;
import io.lettuce.core.models.stream.PendingMessage;
import io.lettuce.core.models.stream.PendingMessages;
import io.lettuce.core.models.stream.PendingParser;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import org.springframework.core.convert.converter.Converter;
@@ -69,9 +64,6 @@ import org.springframework.data.redis.connection.convert.Converters;
import org.springframework.data.redis.connection.convert.ListConverter;
import org.springframework.data.redis.connection.convert.LongToBooleanConverter;
import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.core.types.RedisClientInfo;
@@ -79,7 +71,6 @@ import org.springframework.data.redis.util.ByteUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.NumberUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -657,15 +648,19 @@ abstract public class LettuceConverters extends Converters {
RedisURI.Builder sentinelBuilder = RedisURI.Builder.redis(sentinel.getHost(), sentinel.getPort());
if (sentinelPassword.isPresent()) {
sentinelBuilder.withPassword(sentinelPassword.get());
}
sentinelPassword.toOptional().ifPresent(sentinelBuilder::withPassword);
builder.withSentinel(sentinelBuilder.build());
}
Optional<String> username = sentinelConfiguration.getUsername();
RedisPassword password = sentinelConfiguration.getPassword();
if (password.isPresent()) {
builder.withPassword(password.get());
if (username.isPresent()) {
// See https://github.com/lettuce-io/lettuce-core/issues/1404
builder.withAuthentication(username.get(), new String(password.toOptional().orElse(new char[0])));
} else {
password.toOptional().ifPresent(builder::withPassword);
}
builder.withSentinelMasterId(sentinelConfiguration.getMaster().getName());