Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 24ff543

Browse files
feat: use new eventgrid core for publishing events (#170)
* feat: use new eventgrid core * pr-sug: remove CloudNative.CloudEvents dependency * pr-fix: use new eventgrid publisher in cloudevents integration tests * pr-fix: correct invalid operation exception throwal in az active dir job * pr-fix: update with new az id package * Update Arcus.BackgroundJobs.AzureActiveDirectory.csproj * Update Arcus.BackgroundJobs.AzureActiveDirectory.csproj * pr-add: use eventgrid v3.3 * pr-fix: use correct cloudevent message type in unit tests * Update docs/preview/02-Features/04-AzureActiveDirectory/client-secret-expiration-job.md Co-authored-by: Frederik Gheysels <[email protected]> * Update src/Arcus.BackgroundJobs.AzureActiveDirectory/ClientSecretExpirationJobOptions.cs Co-authored-by: Frederik Gheysels <[email protected]> Co-authored-by: Frederik Gheysels <[email protected]>
1 parent feec9e6 commit 24ff543

26 files changed

+696
-189
lines changed

docs/preview/02-Features/04-AzureActiveDirectory/client-secret-expiration-job.md

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,26 @@ If this is the case either a `ClientSecretAboutToExpire` or `ClientSecretExpired
2323

2424
## Usage
2525

26-
Our background job has to be configured in `ConfigureServices` method:
26+
The background job can easily be added to any .NET hosted application. It is recommended though to create a dedicated background application to run background jobs.
2727

2828
```csharp
29-
using Arcus.EventGrid.Publishing;
30-
using Arcus.EventGrid.Publishing.Interfaces;
29+
using Microsoft.Extensions.Azure;
3130
using Microsoft.Extensions.DependencyInjection;
3231

33-
public class Startup
32+
public class Program
3433
{
3534
public void ConfigureServices(IServiceCollection services)
3635
{
37-
// Make sure that the application has an Arcus EventGrid publisher configured to where the CloudEvents are sent to.
36+
// Make sure that the application has a Microsoft EventGrid publisher client configured to where the CloudEvents are sent to.
3837
// For more information on the Arcus EventGrid publisher: https://eventgrid.arcus-azure.net/Features/publishing-events.
39-
services.AddSingleton<IEventGridPublisher>(serviceProvider =>
38+
services.AddAzureClients(clients =>
4039
{
41-
IEventGridPublisher publisher =
42-
EventGridPublisherBuilder
43-
.ForTopic("<topic-endpoint>")
44-
.UsingAuthenticationKey("<key>")
45-
.Build();
46-
47-
return publisher;
40+
// Arcus provides an additional extension overload that lets us pass-in a secret name instead of the authentication key directly.
41+
// This secret name will be used to contact the Arcus secret store to retrieve the authentication key. (more info: https://security.arcus-azure.net/features/secret-store)
42+
// Note that this Arcus extension needs an correlation system to configure service-to-service correlation. This is by default available in Arcus HTTP middleware and Messaging components (more info: https://observability.arcus-azure.net/Features/correlation).
43+
clients.AddEventGridPublisherClient("https://az-eventgrid-topic-endpoint", "Authentication.Key.Secret.Name");
4844
});
49-
45+
5046
services.AddClientSecretExpirationJob(options =>
5147
{
5248
// The expiration threshold for the client secrets.
@@ -65,6 +61,4 @@ public class Startup
6561
});
6662
}
6763
}
68-
```
69-
70-
[&larr; back](/)
64+
```

src/Arcus.BackgroundJobs.AzureActiveDirectory/Arcus.BackgroundJobs.AzureActiveDirectory.csproj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,22 @@
2626

2727
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
2828
<PackageReference Include="Arcus.EventGrid.Publishing" Version="[3.2.0, 4.0.0)" />
29-
<PackageReference Include="Arcus.Observability.Telemetry.Core" Version="[2.4.0, 3.0.0)" />
29+
<PackageReference Include="Arcus.EventGrid.Core" Version="[3.3.0,4.0.0)" />
30+
<PackageReference Include="Arcus.Observability.Telemetry.Core" Version="[2.5.0, 3.0.0)" />
3031
<PackageReference Include="Arcus.Security.Core" Version="[1.7.0, 2.0.0)" />
3132
<PackageReference Include="Guard.Net" Version="2.0.0" />
3233
</ItemGroup>
3334

3435
<ItemGroup Condition="'$(TargetFramework)' != 'net6.0'">
36+
<PackageReference Include="Arcus.EventGrid.Core" Version="[3.3.0,4.0.0)" />
3537
<PackageReference Include="Arcus.EventGrid.Publishing" Version="[3.1.0, 4.0.0)" />
36-
<PackageReference Include="Arcus.Observability.Telemetry.Core" Version="[2.0.0, 3.0.0)" />
37-
<PackageReference Include="Arcus.Security.Core" Version="[1.6.0, 2.0.0)" />
38+
<PackageReference Include="Arcus.Observability.Telemetry.Core" Version="[2.5.0, 3.0.0)" />
39+
<PackageReference Include="Arcus.Security.Core" Version="[1.7.0, 2.0.0)" />
3840
<PackageReference Include="Guard.Net" Version="1.2.0" />
3941
</ItemGroup>
4042

4143
<ItemGroup>
42-
<PackageReference Include="Azure.Identity" Version="1.4.1" />
44+
<PackageReference Include="Azure.Identity" Version="1.8.0" />
4345
<PackageReference Include="CronScheduler.AspNetCore" Version="3.0.1" />
4446
<PackageReference Include="Microsoft.Graph" Version="4.6.0" />
4547
</ItemGroup>

src/Arcus.BackgroundJobs.AzureActiveDirectory/ClientSecretExpirationJob.cs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
using System.Collections.Generic;
33
using System.Threading;
44
using System.Threading.Tasks;
5-
using Arcus.EventGrid.Publishing.Interfaces;
65
using Azure.Identity;
6+
using Azure.Messaging.EventGrid;
7+
using Arcus.EventGrid.Publishing.Interfaces;
78
using CloudNative.CloudEvents;
89
using CronScheduler.Extensions.Scheduler;
910
using GuardNet;
@@ -19,9 +20,29 @@ namespace Arcus.BackgroundJobs.AzureActiveDirectory
1920
public class ClientSecretExpirationJob : IScheduledJob
2021
{
2122
private readonly ClientSecretExpirationJobSchedulerOptions _options;
22-
private readonly IEventGridPublisher _eventGridPublisher;
23+
private readonly EventGridPublisherClient _publisherClient;
24+
private readonly IEventGridPublisher _eventGridPublisher;
2325
private readonly ILogger<ClientSecretExpirationJob> _logger;
2426

27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="ClientSecretExpirationJob" /> class.
29+
/// </summary>
30+
public ClientSecretExpirationJob(
31+
IOptionsMonitor<ClientSecretExpirationJobSchedulerOptions> options,
32+
EventGridPublisherClient publisherClient,
33+
ILogger<ClientSecretExpirationJob> logger)
34+
{
35+
Guard.NotNull(options, nameof(options));
36+
Guard.NotNull(publisherClient, nameof(publisherClient));
37+
Guard.NotNull(logger, nameof(logger));
38+
39+
ClientSecretExpirationJobSchedulerOptions value = options.Get(Name);
40+
Guard.NotNull(options, nameof(options), "Requires a registered options instance for this background job");
41+
42+
_options = value;
43+
_publisherClient = publisherClient;
44+
_logger = logger;
45+
}
2546
/// <summary>
2647
/// Initializes a new instance of the <see cref="ClientSecretExpirationJob"/> class.
2748
/// </summary>
@@ -32,6 +53,7 @@ public class ClientSecretExpirationJob : IScheduledJob
3253
/// Thrown when the <paramref name="options"/>, <paramref name="eventGridPublisher"/>, <paramref name="logger"/> is <c>null</c>
3354
/// or the <see cref="IOptionsMonitor{TOptions}.Get"/> on the <paramref name="options"/> returns <c>null</c>.
3455
/// </exception>
56+
[Obsolete("Use the constructor overload with the Microsoft " + nameof(EventGridPublisherClient) + " instead")]
3557
public ClientSecretExpirationJob(
3658
IOptionsMonitor<ClientSecretExpirationJobSchedulerOptions> options,
3759
IEventGridPublisher eventGridPublisher,
@@ -140,8 +162,21 @@ private void LogPotentialSecretEvent(AzureApplication application, ClientSecretE
140162

141163
private async Task PublishPotentialSecretEventAsync(AzureApplication application, ClientSecretExpirationEventType eventType)
142164
{
143-
CloudEvent @event = _options.UserOptions.CreateEvent(application, eventType);
144-
await _eventGridPublisher.PublishAsync(@event);
165+
if (_publisherClient != null)
166+
{
167+
Azure.Messaging.CloudEvent @event = _options.UserOptions.CreateCloudEvent(application, eventType);
168+
await _publisherClient.SendEventAsync(@event);
169+
}
170+
else if (_eventGridPublisher != null)
171+
{
172+
CloudEvent @event = _options.UserOptions.CreateEvent(application, eventType);
173+
await _eventGridPublisher.PublishAsync(@event);
174+
}
175+
else
176+
{
177+
throw new InvalidOperationException(
178+
$"Cannot determine EventGrid publisher instance, either use the Microsoft {nameof(EventGridPublisherClient)} or the deprecated Arcus {nameof(IEventGridPublisher)} when initializing this background job");
179+
}
145180
}
146181
}
147182
}

src/Arcus.BackgroundJobs.AzureActiveDirectory/ClientSecretExpirationJobOptions.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using System;
22
using System.Net.Mime;
3+
using Azure.Messaging;
4+
using Azure.Messaging.EventGrid;
35
using CloudNative.CloudEvents;
46
using GuardNet;
57
using Microsoft.Extensions.DependencyInjection;
8+
using CloudEvent = CloudNative.CloudEvents.CloudEvent;
69

710
namespace Arcus.BackgroundJobs.AzureActiveDirectory
811
{
@@ -15,6 +18,7 @@ public class ClientSecretExpirationJobOptions
1518
private bool _runImmediately = false;
1619
private Uri _eventUri = new Uri("https://azure.net/");
1720
private int _expirationThreshold = 14;
21+
private string _clientName = "Default";
1822

1923
/// <summary>
2024
/// Gets or sets the hour which to query for client secrets.
@@ -60,7 +64,7 @@ public Uri EventUri
6064
}
6165

6266
/// <summary>
63-
/// Gets or sets the threshold for the expiration, if the end datetime for a secret is lower than this value a <see cref="CloudEvent"/> will be published.
67+
/// Gets or sets the threshold for the expiration. If the end datetime for a secret is lower than this value a <see cref="CloudNative.CloudEvents.CloudEvent"/> will be published.
6468
/// </summary>
6569
/// <exception cref="ArgumentOutOfRangeException">Thrown when the <paramref name="value"/> is less than zero.</exception>
6670
public int ExpirationThreshold
@@ -75,10 +79,24 @@ public int ExpirationThreshold
7579
}
7680

7781
/// <summary>
78-
/// Creates a <see cref="CloudEvent"/> instance using the predefined values.
82+
/// Gets or sets the logical client name of the registered <see cref="EventGridPublisherClient"/> (default: Default).
83+
/// </summary>
84+
/// <exception cref="ArgumentException">Thrown when the <paramref name="value"/> is blank.</exception>
85+
public string ClientName
86+
{
87+
get => _clientName;
88+
set
89+
{
90+
Guard.NotNullOrWhitespace(value, nameof(value), "Requires a non-blank logical client name of the registered EventGrid publisher client");
91+
_clientName = value;
92+
}
93+
}
94+
95+
/// <summary>
96+
/// Creates a <see cref="CloudNative.CloudEvents.CloudEvent"/> instance using the predefined values.
7997
/// </summary>
8098
/// <param name="application">The <see cref="AzureApplication"/> containing the information regarding the application and its expiring or about to expire secret.</param>
81-
/// <param name="type">The type used in the creation of the <see cref="CloudEvent"/>.</param>
99+
/// <param name="type">The type used in the creation of the <see cref="CloudNative.CloudEvents.CloudEvent"/>.</param>
82100
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="application"/> is null.</exception>
83101
/// <exception cref="ArgumentException">Thrown when the <paramref name="application"/> name is blank.</exception>
84102
internal CloudEvent CreateEvent(AzureApplication application, ClientSecretExpirationEventType type)
@@ -102,5 +120,34 @@ internal CloudEvent CreateEvent(AzureApplication application, ClientSecretExpira
102120

103121
return @event;
104122
}
123+
124+
/// <summary>
125+
/// Creates a <see cref="CloudEvent"/> instance using the predefined values.
126+
/// </summary>
127+
/// <param name="application">The <see cref="AzureApplication"/> containing the information regarding the application and its expiring or about to expire secret.</param>
128+
/// <param name="type">The type used in the creation of the <see cref="CloudEvent"/>.</param>
129+
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="application"/> is null.</exception>
130+
/// <exception cref="ArgumentException">Thrown when the <paramref name="application"/> name is blank.</exception>
131+
internal Azure.Messaging.CloudEvent CreateCloudEvent(AzureApplication application, ClientSecretExpirationEventType type)
132+
{
133+
Guard.NotNull(application, nameof(application));
134+
Guard.NotNullOrWhitespace(application.Name, nameof(application.Name));
135+
136+
string eventSubject = $"/appregistrations/clientsecrets/{application.KeyId}";
137+
string eventId = Guid.NewGuid().ToString();
138+
139+
var @event = new Azure.Messaging.CloudEvent(
140+
_eventUri.OriginalString,
141+
type.ToString(),
142+
BinaryData.FromObjectAsJson(application),
143+
"application/json",
144+
CloudEventDataFormat.Json)
145+
{
146+
Id = eventId,
147+
Subject = eventSubject
148+
};
149+
150+
return @event;
151+
}
105152
}
106153
}

src/Arcus.BackgroundJobs.AzureActiveDirectory/Extensions/IServiceCollectionExtensions.cs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22
using Arcus.BackgroundJobs.AzureActiveDirectory;
3+
using Azure.Messaging.EventGrid;
4+
using Microsoft.Extensions.Azure;
35
using Arcus.EventGrid.Publishing.Interfaces;
46
using GuardNet;
7+
58
using Microsoft.Extensions.Logging;
69
using Microsoft.Extensions.Logging.Abstractions;
710
using Microsoft.Extensions.Options;
@@ -20,28 +23,36 @@ public static class IServiceCollectionExtensions
2023
/// which will query Azure Active Directory for applications that have expired or soon to be expired secrets and send a CloudEvent to an Event Grid Topic.
2124
/// </summary>
2225
/// <remarks>
23-
/// Make sure that you register an <see cref="IEventGridPublisher"/> instance that the background job can use to publish events for potential expired Azure Application secrets.
26+
/// Make sure that you register either an <see cref="EventGridPublisherClient"/> or <see cref="IEventGridPublisher"/> instance
27+
/// that the background job can use to publish events for potential expired Azure Application secrets.
2428
/// For more information on Azure EventGrid, see: <a href="https://eventgrid.arcus-azure.net/Features/publishing-events" />.
2529
/// </remarks>
2630
/// <param name="services">The services to add the background job to.</param>
2731
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <c>null</c>.</exception>
32+
/// <exception cref="InvalidOperationException">
33+
/// Thrown when neither an <see cref="EventGridPublisherClient"/> or <see cref="IEventGridPublisher"/> is registered in the application <paramref name="services"/>.
34+
/// </exception>
2835
public static IServiceCollection AddClientSecretExpirationJob(this IServiceCollection services)
2936
{
3037
Guard.NotNull(services, nameof(services));
3138
return AddClientSecretExpirationJob(services, configureOptions: null);
32-
}
39+
}
3340

3441
/// <summary>
3542
/// Adds the <see cref="ClientSecretExpirationJob"/> scheduled job
3643
/// which will query Azure Active Directory for applications that have expired or soon to be expired secrets and send a CloudEvent to an Event Grid Topic.
3744
/// </summary>
3845
/// <remarks>
39-
/// Make sure that you register an <see cref="IEventGridPublisher"/> instance that the background job can use to publish events for potential expired Azure Application secrets.
46+
/// Make sure that you register either an <see cref="EventGridPublisherClient"/> or <see cref="IEventGridPublisher"/> instance
47+
/// that the background job can use to publish events for potential expired Azure Application secrets.
4048
/// For more information on Azure EventGrid, see: <a href="https://eventgrid.arcus-azure.net/Features/publishing-events" />.
4149
/// </remarks>
4250
/// <param name="services">The services to add the background job to.</param>
4351
/// <param name="configureOptions">The optional additional customized user configuration of options for this background job.</param>
4452
/// <exception cref="ArgumentNullException">Thrown when <paramref name="services"/> is <c>null</c>.</exception>
53+
/// <exception cref="InvalidOperationException">
54+
/// Thrown when neither an <see cref="EventGridPublisherClient"/> or <see cref="IEventGridPublisher"/> is registered in the application <paramref name="services"/>.
55+
/// </exception>
4556
public static IServiceCollection AddClientSecretExpirationJob(
4657
this IServiceCollection services,
4758
Action<ClientSecretExpirationJobOptions> configureOptions)
@@ -54,20 +65,31 @@ public static IServiceCollection AddClientSecretExpirationJob(
5465
serviceProvider =>
5566
{
5667
var options = serviceProvider.GetRequiredService<IOptionsMonitor<ClientSecretExpirationJobSchedulerOptions>>();
57-
var publisher = serviceProvider.GetService<IEventGridPublisher>();
58-
if (publisher is null)
59-
{
60-
throw new InvalidOperationException(
61-
"Could not create a client secret expiration background job because no Arcus EventGrid publisher was registered in the application services, "
62-
+ $"please add an '{nameof(IEventGridPublisher)}' instance to the registered services."
63-
+ "For more information, see: https://eventgrid.arcus-azure.net/Features/publishing-events");
64-
}
65-
6668
var logger =
6769
serviceProvider.GetService<ILogger<ClientSecretExpirationJob>>()
6870
?? NullLogger<ClientSecretExpirationJob>.Instance;
6971

70-
return new ClientSecretExpirationJob(options, publisher, logger);
72+
var factory = serviceProvider.GetService<IAzureClientFactory<EventGridPublisherClient>>();
73+
if (factory != null)
74+
{
75+
ClientSecretExpirationJobOptions userOptions = options.Get(nameof(ClientSecretExpirationJob)).UserOptions;
76+
EventGridPublisherClient client = factory.CreateClient(userOptions.ClientName);
77+
return new ClientSecretExpirationJob(options, client, logger);
78+
}
79+
80+
#pragma warning disable CS0618 // Making sure this functionality is backwards compatible, despite it being deprecated.
81+
var deprecatedClient = serviceProvider.GetService<IEventGridPublisher>();
82+
if (deprecatedClient != null)
83+
{
84+
return new ClientSecretExpirationJob(options, deprecatedClient, logger);
85+
#pragma warning restore CS0618
86+
}
87+
88+
throw new InvalidOperationException(
89+
"Could not create a client secret expiration background job because no Microsoft or Arcus EventGrid publisher was registered in the application services, "
90+
+ $"please add either an '{nameof(EventGridPublisherClient)}' instance via '{nameof(AzureClientFactoryBuilderExtensions.AddEventGridPublisherClient)}'"
91+
+ $"or an '{nameof(IEventGridPublisher)}' instance via 'services.AddEventGridPublisher' to the registered services."
92+
+ "For more information, see: https://eventgrid.arcus-azure.net/Features/publishing-events");
7193
},
7294
options =>
7395
{

src/Arcus.BackgroundJobs.AzureAppConfiguration/Arcus.BackgroundJobs.AzureAppConfiguration.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@
2525
</ItemGroup>
2626

2727
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
28-
<PackageReference Include="Arcus.EventGrid" Version="[3.2.0,4.0.0)" />
2928
<PackageReference Include="Arcus.Messaging.Pumps.ServiceBus" Version="[1.2.0,2.0.0)" />
3029
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
3130
</ItemGroup>
3231

3332
<ItemGroup Condition="'$(TargetFramework)' != 'net6.0'">
34-
<PackageReference Include="Arcus.EventGrid" Version="[3.0.0,4.0.0)" />
3533
<PackageReference Include="Arcus.Messaging.Pumps.ServiceBus" Version="[1.1.0,2.0.0)" />
3634
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
3735
</ItemGroup>

src/Arcus.BackgroundJobs.AzureAppConfiguration/Extensions/IServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using Arcus.BackgroundJobs.AzureAppConfiguration;
33
using Arcus.Messaging.Abstractions.ServiceBus.MessageHandling;
44
using Arcus.Messaging.Pumps.ServiceBus.Configuration;
5-
using CloudNative.CloudEvents;
5+
using Azure.Messaging;
66
using GuardNet;
77
using Microsoft.Extensions.Configuration;
88
using Microsoft.Extensions.Configuration.AzureAppConfiguration;

0 commit comments

Comments
 (0)