Skip to content

Commit 6aa03d6

Browse files
authored
Merge branch 'main' into marc/resp-reader
2 parents c7cfe45 + bc086f3 commit 6aa03d6

10 files changed

Lines changed: 643 additions & 68 deletions

File tree

docs/Authentication.md

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
Authentication
2-
===
1+
# Authentication
32

43
There are multiple ways of connecting to a Redis server, depending on the authentication model. The simplest
54
(but least secure) approach is to use the `default` user, with no authentication, and no transport security.
@@ -12,10 +11,9 @@ var muxer = await ConnectionMultiplexer.ConnectAsync("myserver"); // or myserver
1211
This approach is often used for local transient servers - it is simple, but insecure. But from there,
1312
we can get more complex!
1413

15-
TLS
16-
===
14+
## TLS
1715

18-
If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (AMR, etc), the
16+
If your server has TLS enabled, SE.Redis can be instructed to use it. In some cases (Azure Managed Redis, etc), the
1917
library will recognize the endpoint address, meaning: *you do not need to do anything*. To
2018
*manually* enable TLS, the `ssl` token can be used:
2119

@@ -44,8 +42,7 @@ Alternatively, in advanced scenarios: to provide your own custom server validati
4442
can be used; this uses the normal [`RemoteCertificateValidationCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback)
4543
API.
4644

47-
Usernames and Passwords
48-
===
45+
## Usernames and Passwords
4946

5047
Usernames and passwords can be specified with the `user` and `password` tokens, respectively:
5148

@@ -56,15 +53,25 @@ var muxer = await ConnectionMultiplexer.ConnectAsync("myserver,ssl=true,user=myu
5653
If no `user` is provided, the `default` user is assumed. In some cases, an authentication-token can be
5754
used in place of a classic password.
5855

59-
Client certificates
60-
===
56+
## Managed identities
57+
58+
If the server is an Azure Managed Redis resource, connections can be secured using Microsoft Entra ID authentication. Use the [Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis) extension package to handle the authentication using tokens retrieved from Microsoft Entra. The package integrates via the ConfigurationOptions class, and can use various types of identities for token retrieval. For example with a user-assigned managed identity:
59+
60+
```csharp
61+
var options = ConfigurationOptions.Parse("mycache.region.redis.azure.net:10000");
62+
await options.ConfigureForAzureWithUserAssignedManagedIdentityAsync(managedIdentityClientId);
63+
```
64+
65+
For details and samples see [https://github.com/Azure/Microsoft.Azure.StackExchangeRedis](https://github.com/Azure/Microsoft.Azure.StackExchangeRedis)
66+
67+
## Client certificates
6168

6269
If the server is configured to require a client certificate, this can be supplied in multiple ways.
6370
If you have a local public / private key pair (such as `MyUser2.crt` and `MyUser2.key`), the
6471
`options.SetUserPemCertificate(...)` method can be used:
6572

6673
``` csharp
67-
config.SetUserPemCertificate(
74+
options.SetUserPemCertificate(
6875
userCertificatePath: userCrtPath,
6976
userKeyPath: userKeyPath
7077
);
@@ -74,7 +81,7 @@ If you have a single `pfx` file that contains the public / private pair, the `op
7481
method can be used:
7582

7683
``` csharp
77-
config.SetUserPfxCertificate(
84+
options.SetUserPfxCertificate(
7885
userCertificatePath: userCrtPath,
7986
password: filePassword // optional
8087
);
@@ -85,8 +92,7 @@ can be used; this uses the normal
8592
[`LocalCertificateSelectionCallback`](https://learn.microsoft.com/dotnet/api/system.net.security.remotecertificatevalidationcallback)
8693
API.
8794

88-
User certificates with implicit user authentication
89-
===
95+
## User certificates with implicit user authentication
9096

9197
Historically, the client certificate only provided access to the server, but as the `default` user. From 8.6,
9298
the server can be configured to use client certificates to provide user identity. This replaces the
@@ -114,8 +120,7 @@ var user = (string?)await conn.GetDatabase().ExecuteAsync("acl", "whoami");
114120
Console.WriteLine(user); // writes "MyUser2"
115121
```
116122

117-
More info
118-
===
123+
## More info
119124

120125
For more information:
121126

tests/StackExchange.Redis.Tests/InProcessTestServer.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ protected override void OnMoved(RedisClient client, int hashSlot, Node node)
101101
base.OnMoved(client, hashSlot, node);
102102
}
103103

104+
protected override void OnOutOfBand(RedisClient client, TypedRedisValue message)
105+
{
106+
if (message.IsAggregate
107+
&& message.Span is { IsEmpty: false } span
108+
&& !span[0].IsAggregate)
109+
{
110+
_log?.WriteLine($"Client {client.Id}: {span[0].AsRedisValue()} {message} ");
111+
}
112+
else
113+
{
114+
_log?.WriteLine($"Client {client.Id}: {message}");
115+
}
116+
117+
base.OnOutOfBand(client, message);
118+
}
119+
104120
public override TypedRedisValue OnUnknownCommand(in RedisClient client, in RedisRequest request, ReadOnlySpan<byte> command)
105121
{
106122
_log?.WriteLine($"[{client.Id}] unknown command: {Encoding.ASCII.GetString(command)}");

tests/StackExchange.Redis.Tests/PubSubTests.cs

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,31 @@
1111
namespace StackExchange.Redis.Tests;
1212

1313
[RunPerProtocol]
14-
public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
14+
public class PubSubTests(ITestOutputHelper output, SharedConnectionFixture fixture)
15+
: PubSubTestBase(output, fixture, null)
16+
{
17+
}
18+
19+
/*
20+
[RunPerProtocol]
21+
public class InProcPubSubTests(ITestOutputHelper output, InProcServerFixture fixture)
22+
: PubSubTestBase(output, null, fixture)
23+
{
24+
protected override bool UseDedicatedInProcessServer => false;
25+
}
26+
*/
27+
28+
[RunPerProtocol]
29+
public abstract class PubSubTestBase(
30+
ITestOutputHelper output,
31+
SharedConnectionFixture? connection,
32+
InProcServerFixture? server)
33+
: TestBase(output, connection, server)
1534
{
1635
[Fact]
1736
public async Task ExplicitPublishMode()
1837
{
19-
await using var conn = Create(channelPrefix: "foo:", log: Writer);
38+
await using var conn = ConnectFactory(channelPrefix: "foo:");
2039

2140
var pub = conn.GetSubscriber();
2241
int a = 0, b = 0, c = 0, d = 0;
@@ -54,9 +73,9 @@ await UntilConditionAsync(
5473
[InlineData("Foo:", true, "f")]
5574
public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string breaker)
5675
{
57-
await using var conn = Create(channelPrefix: channelPrefix, shared: false, log: Writer);
76+
await using var conn = ConnectFactory(channelPrefix: channelPrefix, shared: false);
5877

59-
var pub = GetAnyPrimary(conn);
78+
var pub = GetAnyPrimary(conn.DefaultClient);
6079
var sub = conn.GetSubscriber();
6180
await PingAsync(pub, sub).ForAwait();
6281
HashSet<string?> received = [];
@@ -139,10 +158,10 @@ public async Task TestBasicPubSub(string? channelPrefix, bool wildCard, string b
139158
[Fact]
140159
public async Task TestBasicPubSubFireAndForget()
141160
{
142-
await using var conn = Create(shared: false, log: Writer);
161+
await using var conn = ConnectFactory(shared: false);
143162

144-
var profiler = conn.AddProfiler();
145-
var pub = GetAnyPrimary(conn);
163+
var profiler = conn.DefaultClient.AddProfiler();
164+
var pub = GetAnyPrimary(conn.DefaultClient);
146165
var sub = conn.GetSubscriber();
147166

148167
RedisChannel key = RedisChannel.Literal(Me() + Guid.NewGuid());
@@ -214,9 +233,9 @@ private async Task PingAsync(IServer pub, ISubscriber sub, int times = 1)
214233
[Fact]
215234
public async Task TestPatternPubSub()
216235
{
217-
await using var conn = Create(shared: false, log: Writer);
236+
await using var conn = ConnectFactory(shared: false);
218237

219-
var pub = GetAnyPrimary(conn);
238+
var pub = GetAnyPrimary(conn.DefaultClient);
220239
var sub = conn.GetSubscriber();
221240

222241
HashSet<string?> received = [];
@@ -273,7 +292,7 @@ public async Task TestPatternPubSub()
273292
[Fact]
274293
public async Task TestPublishWithNoSubscribers()
275294
{
276-
await using var conn = Create();
295+
await using var conn = ConnectFactory();
277296

278297
var sub = conn.GetSubscriber();
279298
#pragma warning disable CS0618
@@ -285,7 +304,7 @@ public async Task TestPublishWithNoSubscribers()
285304
public async Task TestMassivePublishWithWithoutFlush_Local()
286305
{
287306
Skip.UnlessLongRunning();
288-
await using var conn = Create();
307+
await using var conn = ConnectFactory();
289308

290309
var sub = conn.GetSubscriber();
291310
TestMassivePublish(sub, Me(), "local");
@@ -335,7 +354,7 @@ private void TestMassivePublish(ISubscriber sub, string channel, string caption)
335354
[Fact]
336355
public async Task SubscribeAsyncEnumerable()
337356
{
338-
await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer);
357+
await using var conn = ConnectFactory(shared: false);
339358

340359
var sub = conn.GetSubscriber();
341360
RedisChannel channel = RedisChannel.Literal(Me());
@@ -370,7 +389,7 @@ public async Task SubscribeAsyncEnumerable()
370389
[Fact]
371390
public async Task PubSubGetAllAnyOrder()
372391
{
373-
await using var conn = Create(syncTimeout: 20000, shared: false, log: Writer);
392+
await using var conn = ConnectFactory(shared: false);
374393

375394
var sub = conn.GetSubscriber();
376395
RedisChannel channel = RedisChannel.Literal(Me());
@@ -625,9 +644,10 @@ public async Task PubSubGetAllCorrectOrder_OnMessage_Async()
625644
[Fact]
626645
public async Task TestPublishWithSubscribers()
627646
{
628-
await using var connA = Create(shared: false, log: Writer);
629-
await using var connB = Create(shared: false, log: Writer);
630-
await using var connPub = Create();
647+
await using var pair = ConnectFactory(shared: false);
648+
await using var connA = pair.DefaultClient;
649+
await using var connB = pair.CreateClient();
650+
await using var connPub = pair.CreateClient();
631651

632652
var channel = Me();
633653
var listenA = connA.GetSubscriber();
@@ -652,9 +672,10 @@ public async Task TestPublishWithSubscribers()
652672
[Fact]
653673
public async Task TestMultipleSubscribersGetMessage()
654674
{
655-
await using var connA = Create(shared: false, log: Writer);
656-
await using var connB = Create(shared: false, log: Writer);
657-
await using var connPub = Create();
675+
await using var pair = ConnectFactory(shared: false);
676+
await using var connA = pair.DefaultClient;
677+
await using var connB = pair.CreateClient();
678+
await using var connPub = pair.CreateClient();
658679

659680
var channel = RedisChannel.Literal(Me());
660681
var listenA = connA.GetSubscriber();
@@ -682,7 +703,7 @@ public async Task TestMultipleSubscribersGetMessage()
682703
[Fact]
683704
public async Task Issue38()
684705
{
685-
await using var conn = Create(log: Writer);
706+
await using var conn = ConnectFactory();
686707

687708
var sub = conn.GetSubscriber();
688709
int count = 0;
@@ -717,9 +738,10 @@ public async Task Issue38()
717738
[Fact]
718739
public async Task TestPartialSubscriberGetMessage()
719740
{
720-
await using var connA = Create();
721-
await using var connB = Create();
722-
await using var connPub = Create();
741+
await using var pair = ConnectFactory();
742+
await using var connA = pair.DefaultClient;
743+
await using var connB = pair.CreateClient();
744+
await using var connPub = pair.CreateClient();
723745

724746
int gotA = 0, gotB = 0;
725747
var listenA = connA.GetSubscriber();
@@ -750,8 +772,9 @@ public async Task TestPartialSubscriberGetMessage()
750772
[Fact]
751773
public async Task TestSubscribeUnsubscribeAndSubscribeAgain()
752774
{
753-
await using var connPub = Create();
754-
await using var connSub = Create();
775+
await using var pair = ConnectFactory();
776+
await using var connPub = pair.DefaultClient;
777+
await using var connSub = pair.CreateClient();
755778

756779
var prefix = Me();
757780
var pub = connPub.GetSubscriber();

tests/StackExchange.Redis.Tests/TestBase.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,4 +583,74 @@ protected static async Task UntilConditionAsync(TimeSpan maxWaitTime, Func<bool>
583583
spent += wait;
584584
}
585585
}
586+
587+
// simplified usage to get an interchangeable dedicated vs shared in-process server, useful for debugging
588+
protected virtual bool UseDedicatedInProcessServer => false; // use the shared server by default
589+
internal ClientFactory ConnectFactory(bool allowAdmin = false, string? channelPrefix = null, bool shared = true)
590+
{
591+
if (UseDedicatedInProcessServer)
592+
{
593+
var server = new InProcessTestServer(Output);
594+
return new ClientFactory(this, allowAdmin, channelPrefix, shared, server);
595+
}
596+
return new ClientFactory(this, allowAdmin, channelPrefix, shared, null);
597+
}
598+
599+
internal sealed class ClientFactory : IDisposable, IAsyncDisposable
600+
{
601+
private readonly TestBase _testBase;
602+
private readonly bool _allowAdmin;
603+
private readonly string? _channelPrefix;
604+
private readonly bool _shared;
605+
private readonly InProcessTestServer? _server;
606+
private IInternalConnectionMultiplexer? _defaultClient;
607+
608+
internal ClientFactory(TestBase testBase, bool allowAdmin, string? channelPrefix, bool shared, InProcessTestServer? server)
609+
{
610+
_testBase = testBase;
611+
_allowAdmin = allowAdmin;
612+
_channelPrefix = channelPrefix;
613+
_shared = shared;
614+
_server = server;
615+
}
616+
617+
public IInternalConnectionMultiplexer DefaultClient => _defaultClient ??= CreateClient();
618+
619+
public InProcessTestServer? Server => _server;
620+
621+
public IInternalConnectionMultiplexer CreateClient()
622+
{
623+
if (_server is not null)
624+
{
625+
var config = _server.GetClientConfig();
626+
config.AllowAdmin = _allowAdmin;
627+
if (_channelPrefix is not null)
628+
{
629+
config.ChannelPrefix = RedisChannel.Literal(_channelPrefix);
630+
}
631+
return ConnectionMultiplexer.ConnectAsync(config).Result;
632+
}
633+
return _testBase.Create(allowAdmin: _allowAdmin, channelPrefix: _channelPrefix, shared: _shared);
634+
}
635+
636+
public IDatabase GetDatabase(int db = -1) => DefaultClient.GetDatabase(db);
637+
638+
public ISubscriber GetSubscriber() => DefaultClient.GetSubscriber();
639+
640+
public void Dispose()
641+
{
642+
_server?.Dispose();
643+
_defaultClient?.Dispose();
644+
}
645+
646+
public ValueTask DisposeAsync()
647+
{
648+
_server?.Dispose();
649+
if (_defaultClient is not null)
650+
{
651+
return _defaultClient.DisposeAsync();
652+
}
653+
return default;
654+
}
655+
}
586656
}

0 commit comments

Comments
 (0)