Skip to content

Commit 950b1c1

Browse files
Add Azure Entra authorization (#103)
1 parent 235cff6 commit 950b1c1

10 files changed

Lines changed: 162 additions & 23 deletions

File tree

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## 5.4.0
8+
- Added
9+
- Introduced a new way to connect to Service Bus using Azure Entra authentication.
710
## 5.3.1
811
- Changed
912
- Fix usage of sent counter on receiver instead of received counter

src/Ev.ServiceBus.Abstractions/Configuration/ConnectionSettings.cs

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
using System;
1+
using Azure.Core;
22
using Azure.Messaging.ServiceBus;
3+
using System;
34

45
namespace Ev.ServiceBus.Abstractions;
56

@@ -12,9 +13,23 @@ internal ConnectionSettings(string connectionString, ServiceBusClientOptions opt
1213
Endpoint = GetEndpointFromConnectionString(connectionString);
1314
}
1415

16+
internal ConnectionSettings(string fullyQualifiedNamespace, TokenCredential credentials, ServiceBusClientOptions options)
17+
{
18+
Options = options;
19+
FullyQualifiedNamespace = fullyQualifiedNamespace;
20+
Credentials = credentials;
21+
Endpoint = GetEndpointFromFullyQualifiedNamespace(fullyQualifiedNamespace);
22+
}
23+
1524
public string Endpoint { get; }
16-
public string ConnectionString { get; }
17-
public ServiceBusClientOptions Options { get; }
25+
26+
public string? ConnectionString { get; }
27+
28+
public ServiceBusClientOptions? Options { get; }
29+
30+
public string? FullyQualifiedNamespace { get; }
31+
32+
public TokenCredential? Credentials { get; }
1833

1934
private string GetEndpointFromConnectionString(string connectionString)
2035
{
@@ -43,17 +58,34 @@ private string GetEndpointFromConnectionString(string connectionString)
4358
return string.Empty;
4459
}
4560

46-
public override int GetHashCode()
61+
private string GetEndpointFromFullyQualifiedNamespace(string fullyQualifiedNamespace)
4762
{
48-
return Endpoint.GetHashCode();
63+
return $"sb://{fullyQualifiedNamespace}/";
4964
}
5065

66+
private bool Equals(ConnectionSettings other) =>
67+
string.Equals(Endpoint, other.Endpoint, StringComparison.Ordinal)
68+
&& string.Equals(ConnectionString, other.ConnectionString, StringComparison.Ordinal)
69+
&& Options != null
70+
&& Options.Equals(other.Options)
71+
&& string.Equals(FullyQualifiedNamespace, other.FullyQualifiedNamespace, StringComparison.Ordinal)
72+
&& Equals(Credentials, other.Credentials);
73+
5174
public override bool Equals(object? obj)
5275
{
53-
if (obj is not ConnectionSettings settings)
54-
{
55-
return false;
56-
}
57-
return Endpoint.Equals(settings.Endpoint);
76+
if (obj is null) return false;
77+
if (ReferenceEquals(this, obj)) return true;
78+
if (obj.GetType() != GetType()) return false;
79+
return Equals((ConnectionSettings)obj);
80+
}
81+
82+
public override int GetHashCode()
83+
{
84+
return HashCode.Combine(
85+
Endpoint,
86+
ConnectionString,
87+
Options,
88+
FullyQualifiedNamespace,
89+
Credentials);
5890
}
5991
}

src/Ev.ServiceBus.Abstractions/Configuration/Extensions/ClientOptionsExtensions.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Azure.Messaging.ServiceBus;
1+
using Azure.Core;
2+
using Azure.Messaging.ServiceBus;
23

34
// ReSharper disable once CheckNamespace
45
namespace Ev.ServiceBus.Abstractions;
@@ -23,4 +24,25 @@ public static TOptions WithConnection<TOptions>(
2324
options.ConnectionSettings = new ConnectionSettings(connectionString, connectionOptions);
2425
return options;
2526
}
27+
28+
/// <summary>
29+
/// Sets the connection to use for this resource using Azure Entra ID.
30+
/// If no connection is set then the default connection will be used.
31+
/// </summary>
32+
/// <param name="options"></param>
33+
/// <param name="fullyQualifiedNamespace"></param>
34+
/// <param name="credentials"></param>
35+
/// <param name="connectionOptions"></param>
36+
/// <typeparam name="TOptions"></typeparam>
37+
/// <returns></returns>
38+
public static TOptions WithConnection<TOptions>(
39+
this TOptions options,
40+
string fullyQualifiedNamespace,
41+
TokenCredential credentials,
42+
ServiceBusClientOptions connectionOptions)
43+
where TOptions : ClientOptions
44+
{
45+
options.ConnectionSettings = new ConnectionSettings(fullyQualifiedNamespace, credentials, connectionOptions);
46+
return options;
47+
}
2648
}

src/Ev.ServiceBus.Abstractions/Configuration/ServiceBusSettings.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Azure.Messaging.ServiceBus;
1+
using Azure.Core;
2+
using Azure.Messaging.ServiceBus;
23

34
namespace Ev.ServiceBus.Abstractions;
45

@@ -35,6 +36,11 @@ public void WithConnection(string connectionString, ServiceBusClientOptions opti
3536
ConnectionSettings = new ConnectionSettings(connectionString, options);
3637
}
3738

39+
public void WithConnection(string fullyQualifiedNamespace, TokenCredential tokenCredential, ServiceBusClientOptions options)
40+
{
41+
ConnectionSettings = new ConnectionSettings(fullyQualifiedNamespace, tokenCredential, options);
42+
}
43+
3844
public void WithIsolation(IsolationBehavior behavior, string? isolationKey = null, string? applicationName = null)
3945
{
4046
IsolationSettings = new IsolationSettings(behavior, isolationKey, applicationName);

src/Ev.ServiceBus.Abstractions/Ev.ServiceBus.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<Nullable>enable</Nullable>
1515
</PropertyGroup>
1616
<ItemGroup>
17+
<PackageReference Include="Azure.Identity" Version="1.17.0" />
1718
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
1819
</ItemGroup>
1920

src/Ev.ServiceBus.HealthChecks/ConnectionSettingsComparer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@ public bool Equals(ConnectionSettings? x, ConnectionSettings? y)
2828
return false;
2929
}
3030

31-
return x.ConnectionString == y.ConnectionString;
31+
return x.ConnectionString == y.ConnectionString
32+
&& x.FullyQualifiedNamespace == y.FullyQualifiedNamespace
33+
&& x.Credentials == y.Credentials;
3234
}
3335

3436
public int GetHashCode(ConnectionSettings? obj)
3537
{
36-
return obj?.ConnectionString?.GetHashCode() ?? 0;
38+
return HashCode.Combine(
39+
obj?.ConnectionString,
40+
obj?.FullyQualifiedNamespace,
41+
obj?.Credentials);
3742
}
3843
}

src/Ev.ServiceBus.HealthChecks/RegistrationService.cs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,21 @@ public void Configure(HealthCheckServiceOptions options)
2727
return;
2828
}
2929

30-
var commonConnectionString = _serviceBusOptions.Value.Settings.ConnectionSettings?.ConnectionString;
30+
var commonSettings = _serviceBusOptions.Value.Settings.ConnectionSettings;
31+
var commonConnectionString = commonSettings?.ConnectionString;
32+
var commonFullyQualifiedNamespace = commonSettings?.FullyQualifiedNamespace;
33+
var commonCredentials = commonSettings?.Credentials;
34+
3135
var resources = _serviceBusOptions.Value.Receivers.Union(_serviceBusOptions.Value.Senders).Distinct()
3236
.ToArray();
3337

3438
foreach (var resourceGroup in resources.GroupBy(o => o.ConnectionSettings, new ConnectionSettingsComparer()))
3539
{
3640
var connectionString = resourceGroup.Key?.ConnectionString ?? commonConnectionString;
37-
if (connectionString == null)
41+
var fullyQualifiedNamespace = resourceGroup.Key?.FullyQualifiedNamespace ?? commonFullyQualifiedNamespace;
42+
var credentials = resourceGroup.Key?.Credentials ?? commonCredentials;
43+
44+
if (connectionString == null && fullyQualifiedNamespace == null && credentials == null)
3845
{
3946
continue;
4047
}
@@ -46,7 +53,9 @@ public void Configure(HealthCheckServiceOptions options)
4653
options.Registrations.Add(new HealthCheckRegistration($"Queue:{group.Key}",
4754
sp => (IHealthCheck) new AzureServiceBusQueueHealthCheck(new AzureServiceBusQueueHealthCheckOptions(group.Key)
4855
{
49-
ConnectionString = connectionString
56+
ConnectionString = connectionString,
57+
FullyQualifiedNamespace = fullyQualifiedNamespace,
58+
Credential = credentials
5059
}),
5160
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
5261
}
@@ -58,7 +67,9 @@ public void Configure(HealthCheckServiceOptions options)
5867
options.Registrations.Add(new HealthCheckRegistration($"Topic:{group.Key}",
5968
sp => (IHealthCheck) new AzureServiceBusTopicHealthCheck(new AzureServiceBusTopicHealthCheckOptions(group.Key)
6069
{
61-
ConnectionString = connectionString
70+
ConnectionString = connectionString,
71+
FullyQualifiedNamespace = fullyQualifiedNamespace,
72+
Credential = credentials,
6273
}),
6374
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
6475
}
@@ -73,7 +84,9 @@ public void Configure(HealthCheckServiceOptions options)
7384
options.Registrations.Add(new HealthCheckRegistration($"Subscription:{group.Key.TopicName}/Subscriptions/{group.Key.SubscriptionName}",
7485
sp => (IHealthCheck) new AzureServiceBusSubscriptionHealthCheck(new AzureServiceBusSubscriptionHealthCheckHealthCheckOptions(group.Key.TopicName, group.Key.SubscriptionName)
7586
{
76-
ConnectionString = connectionString
87+
ConnectionString = connectionString,
88+
FullyQualifiedNamespace = fullyQualifiedNamespace,
89+
Credential = credentials
7790
}),
7891
null, HealthChecksBuilderExtensions.HealthCheckTags, null));
7992
}

src/Ev.ServiceBus/Dispatch/DispatchRegistrationBuilder.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ public void CustomizeConnection(
2828
_options.WithConnection(connectionString, options);
2929
}
3030

31+
/// <summary>
32+
/// Sets a specific connection using Azure Entra authorization for the underlying resource.
33+
/// </summary>
34+
/// <param name="fullyQualifiedNamespace"></param>
35+
/// <param name="credentials"></param>
36+
/// <param name="options"></param>
37+
public void CustomizeConnection(
38+
string fullyQualifiedNamespace,
39+
Azure.Core.TokenCredential credentials,
40+
ServiceBusClientOptions options)
41+
{
42+
_options.WithConnection(fullyQualifiedNamespace, credentials, options);
43+
}
44+
3145
/// <summary>
3246
/// Registers a class as a payload to serialize and send through the current resource.
3347
/// </summary>
Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using Azure.Messaging.ServiceBus;
23
using Ev.ServiceBus.Abstractions;
34

@@ -7,11 +8,20 @@ public class ClientFactory : IClientFactory
78
{
89
public ServiceBusClient Create(ConnectionSettings connectionSettings)
910
{
10-
if (connectionSettings.Options != null)
11+
if (!string.IsNullOrWhiteSpace(connectionSettings.ConnectionString))
1112
{
12-
return new ServiceBusClient(connectionSettings.ConnectionString, connectionSettings.Options);
13+
return connectionSettings.Options is not null
14+
? new ServiceBusClient(connectionSettings.ConnectionString, connectionSettings.Options)
15+
: new ServiceBusClient(connectionSettings.ConnectionString);
1316
}
1417

15-
return new ServiceBusClient(connectionSettings.ConnectionString);
18+
if(connectionSettings.Credentials is not null && !string.IsNullOrWhiteSpace(connectionSettings.FullyQualifiedNamespace))
19+
{
20+
return connectionSettings.Options is not null
21+
? new ServiceBusClient(connectionSettings.FullyQualifiedNamespace, connectionSettings.Credentials, connectionSettings.Options)
22+
: new ServiceBusClient(connectionSettings.FullyQualifiedNamespace, connectionSettings.Credentials);
23+
}
24+
25+
throw new InvalidOperationException("Insufficient connection settings: provide either a connection string or both FullyQualifiedNamespace and Credentials.");
1626
}
17-
}
27+
}

tests/Ev.ServiceBus.HealthChecks.UnitTests/ComplexTest.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Threading;
22
using System.Threading.Tasks;
3+
using Azure.Identity;
34
using Azure.Messaging.ServiceBus;
45
using Ev.ServiceBus.Reception;
56
using Ev.ServiceBus.UnitTests.Helpers;
@@ -166,6 +167,38 @@ public void MultipleRegistrationsGoesWell_case4()
166167
reg => { reg.Name.Should().Be("Subscription:topic/Subscriptions/subscription3"); });
167168
}
168169

170+
[Fact]
171+
public void HealthCheckWorksWithEntraAuthorization()
172+
{
173+
var services = new ServiceCollection();
174+
175+
services.AddServiceBus(settings =>
176+
{
177+
settings.WithConnection("fullyQualifiedNamespace", new DefaultAzureCredential(), new ServiceBusClientOptions());
178+
});
179+
180+
services.AddHealthChecks().AddEvServiceBusChecks();
181+
182+
services.RegisterServiceBusReception().FromSubscription("topic", "subscription", builder =>
183+
{
184+
builder.RegisterReception<RelationCreated, RelationCreatedHandler>();
185+
});
186+
services.RegisterServiceBusReception().FromQueue("queue", builder =>
187+
{
188+
builder.RegisterReception<RelationCreated, RelationCreatedHandler>();
189+
});
190+
191+
var provider = services.BuildServiceProvider();
192+
193+
var healthOptions = provider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
194+
195+
healthOptions.Value.Registrations.Count.Should().Be(2);
196+
healthOptions.Value.Registrations.Should().SatisfyRespectively(
197+
reg => { reg.Name.Should().Be("Queue:queue"); },
198+
reg => { reg.Name.Should().Be("Subscription:topic/Subscriptions/subscription"); }
199+
);
200+
}
201+
169202
public class RelationCreated { }
170203

171204
public class RelationCreatedHandler : IMessageReceptionHandler<RelationCreated>

0 commit comments

Comments
 (0)