Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ The `ConfigurationOptions` object has a wide range of properties, all of which a
| tcpKeepAlive={bool} | `TcpKeepAlive` | `true` | Enables TCP keep-alive when appropriate (endpoint- and platform-dependent) |
| name={string} | `ClientName` | `null` | Identification for the connection within redis |
| password={string} | `Password` | `null` | Password for the redis server |
| sentinelPassword={string} | `SentinelPassword` | `null` | Optional password to authenticate with Sentinel servers (falls back to `password` if not provided) |
| user={string} | `User` | `null` | User for the redis server (for use with ACLs on redis 6 and above) |
| sentinelUser={string} | `SentinelUser` | `null` | Optional username to authenticate with Sentinel servers (falls back to `user` if not provided) |
| proxy={proxy type} | `Proxy` | `Proxy.None` | Type of proxy in use (if any); for example "twemproxy/envoyproxy" |
| resolveDns={bool} | `ResolveDns` | `false` | Specifies that DNS resolution should be explicit and eager, rather than implicit |
| serviceName={string} | `ServiceName` | `null` | Used for connecting to a sentinel primary service |
Expand Down
40 changes: 38 additions & 2 deletions src/StackExchange.Redis/ConfigurationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ internal const string
KeepAlive = "keepAlive",
ClientName = "name",
User = "user",
SentinelUser = "sentinelUser",
Password = "password",
SentinelPassword = "sentinelPassword",
PreserveAsyncOrder = "preserveAsyncOrder",
Proxy = "proxy",
ResolveDns = "resolveDns",
Expand Down Expand Up @@ -141,7 +143,9 @@ internal const string
HighPrioritySocketThreads,
KeepAlive,
User,
SentinelUser,
Password,
SentinelPassword,
PreserveAsyncOrder,
Proxy,
ResolveDns,
Expand Down Expand Up @@ -217,7 +221,7 @@ private enum OptionFlags : ulong

private OptionFlags optionFlags;

private string? tieBreaker, sslHost, configChannel, user, password;
private string? tieBreaker, sslHost, configChannel, user, sentinelUser, password, sentinelPassword;
Comment thread
doosterkamp marked this conversation as resolved.

private TimeSpan heartbeatInterval;

Expand Down Expand Up @@ -746,6 +750,16 @@ public string? User
set => user = value;
}

/// <summary>
/// The username to use to authenticate with Sentinel servers, only when different from the Redis server password (optional).
/// If not specified, <see cref="User"/> is used when communicating with Sentinels.
/// </summary>
public string? SentinelUser
{
get => sentinelUser ?? user ?? Defaults.User;
set => sentinelUser = value;
}

/// <summary>
/// The password to use to authenticate with the server.
/// </summary>
Expand All @@ -755,6 +769,16 @@ public string? Password
set => password = value;
}

/// <summary>
/// The password to use to authenticate with Sentinel servers, only when different from the Redis server password (optional).
/// If not specified, <see cref="Password"/> is used when communicating with Sentinels.
/// </summary>
public string? SentinelPassword
{
get => sentinelPassword ?? password ?? Defaults.Password;
set => sentinelPassword = value;
}

/// <summary>
/// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order.
/// </summary>
Expand Down Expand Up @@ -941,7 +965,7 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
public ConfigurationOptions Clone() => new ConfigurationOptions
{
defaultOptions = defaultOptions,
optionFlags = this.optionFlags,
optionFlags = optionFlags,
ClientName = ClientName,
ServiceName = ServiceName,
keepAlive = keepAlive,
Expand All @@ -950,7 +974,9 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
defaultVersion = defaultVersion,
connectTimeout = connectTimeout,
user = user,
sentinelUser = sentinelUser,
password = password,
sentinelPassword = sentinelPassword,
tieBreaker = tieBreaker,
sslHost = sslHost,
configChannel = configChannel,
Expand Down Expand Up @@ -1051,7 +1077,9 @@ public string ToString(bool includePassword)
Append(sb, OptionKeys.Version, defaultVersion);
Append(sb, OptionKeys.ConnectTimeout, OptionFlags.ConnectTimeoutHasValue, in connectTimeout);
Append(sb, OptionKeys.User, user);
Append(sb, OptionKeys.SentinelUser, sentinelUser);
Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(password)) ? password : "*****");
Append(sb, OptionKeys.SentinelPassword, (includePassword || string.IsNullOrEmpty(sentinelPassword)) ? sentinelPassword : "*****");
Append(sb, OptionKeys.TieBreaker, tieBreaker);
Append(sb, OptionKeys.Ssl, OptionFlags.SslHasValue, OptionFlags.SslValue);
if (HasValue(OptionFlags.SslProtocolsHasValue)) Append(sb, OptionKeys.SslProtocols, sslProtocols.ToString().Replace(',', '|'));
Expand Down Expand Up @@ -1190,6 +1218,8 @@ private void Clear()
#if DEBUG
OutputLog = null;
#endif
sentinelUser = null;
sentinelPassword = null;
}

object ICloneable.Clone() => Clone();
Expand Down Expand Up @@ -1274,9 +1304,15 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
case OptionKeys.User:
user = value;
break;
case OptionKeys.SentinelUser:
SentinelUser = value;
break;
case OptionKeys.Password:
password = value;
break;
case OptionKeys.SentinelPassword:
SentinelPassword = value;
break;
case OptionKeys.TieBreaker:
TieBreaker = value;
break;
Expand Down
16 changes: 13 additions & 3 deletions src/StackExchange.Redis/ConnectionMultiplexer.Sentinel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public partial class ConnectionMultiplexer
/// <param name="log">The <see cref="ILogger"/> to log to, if any.</param>
internal void InitializeSentinel(ILogger? log)
{
if (ServerSelectionStrategy.ServerType != ServerType.Sentinel)
if (!_isSentinel)
{
return;
}
Expand Down Expand Up @@ -144,7 +144,12 @@ public static Task<ConnectionMultiplexer> SentinelConnectAsync(ConfigurationOpti
/// <param name="log">The <see cref="TextWriter"/> to log to.</param>
private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions configuration, TextWriter? log = null)
{
var sentinelConnection = SentinelConnect(configuration, log);
// Use separate sentinel credentials when provided on the configuration
var sentinelConfig = configuration.Clone();
sentinelConfig.User = configuration.SentinelUser;
sentinelConfig.Password = configuration.SentinelPassword;

var sentinelConnection = SentinelConnect(sentinelConfig, log);

var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, log);
// Set reference to sentinel connection so that we can dispose it
Expand All @@ -161,7 +166,12 @@ private static ConnectionMultiplexer SentinelPrimaryConnect(ConfigurationOptions
/// <param name="writer">The <see cref="TextWriter"/> to log to.</param>
private static async Task<ConnectionMultiplexer> SentinelPrimaryConnectAsync(ConfigurationOptions configuration, TextWriter? writer = null)
{
var sentinelConnection = await SentinelConnectAsync(configuration, writer).ForAwait();
// Use separate sentinel credentials when provided on the configuration
var sentinelConfig = configuration.Clone();
sentinelConfig.User = configuration.SentinelUser;
sentinelConfig.Password = configuration.SentinelPassword;

var sentinelConnection = await SentinelConnectAsync(sentinelConfig, writer).ForAwait();

var muxer = sentinelConnection.GetSentinelMasterConnection(configuration, writer);
// Set reference to sentinel connection so that we can dispose it
Expand Down
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/ConnectionMultiplexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public sealed partial class ConnectionMultiplexer : IInternalConnectionMultiplex
internal bool IsDisposed => _isDisposed;
internal ILogger<ConnectionMultiplexer>? Logger { get; }

private readonly bool _isSentinel;

internal CommandMap CommandMap { get; }
internal EndPointCollection EndPoints { get; }
internal ConfigurationOptions RawConfig { get; }
Expand Down Expand Up @@ -150,6 +152,8 @@ private ConnectionMultiplexer(ConfigurationOptions configuration, ServerType? se
EndPoints.SetDefaultPorts(serverType, ssl: RawConfig.Ssl);
Logger = configuration.LoggerFactory?.CreateLogger<ConnectionMultiplexer>();

_isSentinel = serverType == ServerType.Sentinel;

var map = CommandMap = configuration.GetCommandMap(serverType);
if (!string.IsNullOrWhiteSpace(configuration.Password) && !configuration.TryResp3()) // RESP3 doesn't need AUTH (can issue as part of HELLO)
{
Expand Down
4 changes: 4 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#nullable enable
StackExchange.Redis.ConfigurationOptions.SentinelPassword.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelPassword.set -> void
StackExchange.Redis.ConfigurationOptions.SentinelUser.get -> string?
StackExchange.Redis.ConfigurationOptions.SentinelUser.set -> void
[SER009]virtual StackExchange.Redis.Configuration.Tunnel.ConnectTransportAsync(System.Net.EndPoint! endpoint, StackExchange.Redis.ConnectionType connectionType, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask<RESPite.Transports.DuplexTransport?>
9 changes: 5 additions & 4 deletions src/StackExchange.Redis/ServerEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -959,13 +959,14 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
Multiplexer.Trace("No connection!?");
return;
}

Message msg;
// Note that we need "" (not null) for password in the case of 'nopass' logins
var config = Multiplexer.RawConfig;
string? user = config.User;
string password = config.Password ?? "";
var user = config.User;
// Note that we need "" (not null) for password in the case of 'nopass' logins
var password = config.Password ?? "";
var clientName = Multiplexer.ClientName;

string clientName = Multiplexer.ClientName;
if (!string.IsNullOrWhiteSpace(clientName))
{
clientName = nameSanitizer.Replace(clientName, "");
Expand Down
2 changes: 2 additions & 0 deletions tests/StackExchange.Redis.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ orderby name
"ResponseBufferPool",
"responseTimeout",
"RetryPolicy",
"sentinelPassword",
"sentinelUser",
"ServiceName",
"SocketManager",
#if !NETFRAMEWORK
Expand Down
47 changes: 47 additions & 0 deletions tests/StackExchange.Redis.Tests/SentinelConfigTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using Xunit;

namespace StackExchange.Redis.Tests;

public class SentinelConfigTests
{
[Fact]
public void Parse_SentinelCredentials_FromConnectionString()
{
var cs = "localhost:26379,serviceName=myprimary,sentinelUser=su,sentinelPassword=sp";
var options = ConfigurationOptions.Parse(cs);

Assert.Equal("su", options.SentinelUser);
Assert.Equal("sp", options.SentinelPassword);
Assert.Equal("myprimary", options.ServiceName);
}

[Fact]
public void ToString_Masks_SentinelPassword_WhenExcluded()
{
var options = new ConfigurationOptions();
options.EndPoints.Add("localhost", 26379);
options.ServiceName = "myprimary";
options.SentinelUser = "su";
options.SentinelPassword = "secret";

var repr = options.ToString(includePassword: false);

Assert.Contains("sentinelUser=su", repr);
Assert.Contains("sentinelPassword=*****", repr);
Assert.DoesNotContain("secret", repr);
}

[Fact]
public void Clone_Preserves_SentinelCredentials()
{
var options = new ConfigurationOptions();
options.SentinelUser = "su";
options.SentinelPassword = "sp";

var clone = options.Clone();

Assert.Equal(options.SentinelUser, clone.SentinelUser);
Assert.Equal(options.SentinelPassword, clone.SentinelPassword);
}
}
Loading