Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Arcus.BackgroundJobs.KeyVault;
using Arcus.BackgroundJobs.KeyVault.Events;
using Arcus.Messaging.Pumps.ServiceBus;
using Arcus.Messaging.Pumps.ServiceBus.Configuration;
using Arcus.Security.Core.Caching;
using CloudNative.CloudEvents;
using GuardNet;
Expand All @@ -24,6 +25,9 @@ public static class IServiceCollectionExtensions
/// <param name="services">The services collection to add the job to.</param>
/// <param name="subscriptionNamePrefix">The name of the Azure Service Bus subscription that will be created to receive Key Vault events.</param>
/// <param name="serviceBusTopicConnectionStringSecretKey">The configuration key that points to the Azure Service Bus Topic connection string.</param>
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="subscriptionNamePrefix"/> or <paramref name="serviceBusTopicConnectionStringSecretKey"/> is blank.
/// </exception>
public static IServiceCollection AddAutoInvalidateKeyVaultSecretBackgroundJob(
this IServiceCollection services,
string subscriptionNamePrefix,
Expand All @@ -32,7 +36,29 @@ public static IServiceCollection AddAutoInvalidateKeyVaultSecretBackgroundJob(
Guard.NotNullOrWhitespace(subscriptionNamePrefix, nameof(subscriptionNamePrefix), "Requires a non-blank subscription name of the Azure Service Bus Topic subscription, to receive Key Vault events");
Guard.NotNullOrWhitespace(serviceBusTopicConnectionStringSecretKey, nameof(serviceBusTopicConnectionStringSecretKey), "Requires a non-blank configuration key that points to a Azure Service Bus Topic");

services.AddCloudEventBackgroundJob(subscriptionNamePrefix, serviceBusTopicConnectionStringSecretKey)
return AddAutoInvalidateKeyVaultSecretBackgroundJob(services, subscriptionNamePrefix, serviceBusTopicConnectionStringSecretKey, configureBackgroundJob: null);
}

/// <summary>
/// Adds a background job to the <see cref="IServiceCollection"/> to automatically invalidate cached Azure Key Vault secrets.
/// </summary>
/// <param name="services">The services collection to add the job to.</param>
/// <param name="subscriptionNamePrefix">The name of the Azure Service Bus subscription that will be created to receive Key Vault events.</param>
/// <param name="serviceBusTopicConnectionStringSecretKey">The configuration key that points to the Azure Service Bus Topic connection string.</param>
/// <param name="configureBackgroundJob">The capability to configure additional options on how the auto-invalidate Azure Key Vault background job should behave.</param>
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="subscriptionNamePrefix"/> or <paramref name="serviceBusTopicConnectionStringSecretKey"/> is blank.
/// </exception>
public static IServiceCollection AddAutoInvalidateKeyVaultSecretBackgroundJob(
this IServiceCollection services,
string subscriptionNamePrefix,
string serviceBusTopicConnectionStringSecretKey,
Action<IAzureServiceBusTopicMessagePumpOptions> configureBackgroundJob)
{
Guard.NotNullOrWhitespace(subscriptionNamePrefix, nameof(subscriptionNamePrefix), "Requires a non-blank subscription name of the Azure Service Bus Topic subscription, to receive Key Vault events");
Guard.NotNullOrWhitespace(serviceBusTopicConnectionStringSecretKey, nameof(serviceBusTopicConnectionStringSecretKey), "Requires a non-blank configuration key that points to a Azure Service Bus Topic");

services.AddCloudEventBackgroundJob(subscriptionNamePrefix, serviceBusTopicConnectionStringSecretKey, configureBackgroundJob)
.WithServiceBusMessageHandler<InvalidateKeyVaultSecretHandler, CloudEvent>(
messageBodyFilter: cloudEvent => cloudEvent?.GetPayload<SecretNewVersionCreated>() != null,
implementationFactory: serviceProvider =>
Expand All @@ -44,7 +70,7 @@ public static IServiceCollection AddAutoInvalidateKeyVaultSecretBackgroundJob(

return services;
}

/// <summary>
/// Adds a background job to the <see cref="IServiceCollection"/> to automatically restart a <see cref="AzureServiceBusMessagePump"/> with a specific <paramref name="jobId"/>
/// when the Azure Key Vault secret that holds the Azure Service Bus connection string was updated.
Expand Down Expand Up @@ -75,7 +101,51 @@ public static IServiceCollection AddAutoRestartServiceBusMessagePumpOnRotatedCre
Guard.NotNullOrWhitespace(serviceBusTopicConnectionStringSecretKey, nameof(serviceBusTopicConnectionStringSecretKey), "Requires a non-blank secret key that points to a Azure Service Bus Topic");
Guard.NotNullOrWhitespace(messagePumpConnectionStringKey, nameof(messagePumpConnectionStringKey), "Requires a non-blank secret key that points to the credentials that holds the connection string of the target message pump");

services.AddCloudEventBackgroundJob(subscriptionNamePrefix, serviceBusTopicConnectionStringSecretKey)
return AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
services,
jobId,
subscriptionNamePrefix,
serviceBusTopicConnectionStringSecretKey,
messagePumpConnectionStringKey,
configureBackgroundJob: null);
}

/// <summary>
/// Adds a background job to the <see cref="IServiceCollection"/> to automatically restart a <see cref="AzureServiceBusMessagePump"/> with a specific <paramref name="jobId"/>
/// when the Azure Key Vault secret that holds the Azure Service Bus connection string was updated.
/// </summary>
/// <param name="services">The collection of services to add the job to.</param>
/// <param name="jobId">The unique background job ID to identify which message pump to restart.</param>
/// <param name="subscriptionNamePrefix">The name of the Azure Service Bus subscription that will be created to receive <see cref="CloudEvent"/>'s.</param>
/// <param name="serviceBusTopicConnectionStringSecretKey">The secret key that points to the Azure Service Bus Topic connection string.</param>
/// <param name="messagePumpConnectionStringKey">
/// The secret key where the connection string credentials are located for the target message pump that needs to be auto-restarted.
/// </param>
/// <param name="configureBackgroundJob">
/// The capability to configure additional options on how the auto-restart Azure Service Bus message pump
/// on rotated Azure Key Vault credentials background job should behave.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when the <paramref name="services"/> or the searched for <see cref="AzureServiceBusMessagePump"/> based on the given <paramref name="jobId"/> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when the <paramref name="subscriptionNamePrefix"/> or <paramref name="serviceBusTopicConnectionStringSecretKey"/> is blank.
/// </exception>
public static IServiceCollection AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
this IServiceCollection services,
string jobId,
string subscriptionNamePrefix,
string serviceBusTopicConnectionStringSecretKey,
string messagePumpConnectionStringKey,
Action<IAzureServiceBusTopicMessagePumpOptions> configureBackgroundJob)
{
Guard.NotNull(services, nameof(services), "Requires a collection of services to add the re-authentication background job");
Guard.NotNullOrWhitespace(jobId, nameof(jobId), "Requires a non-blank job ID to identify the Azure Service Bus message pump which needs to restart");
Guard.NotNullOrWhitespace(subscriptionNamePrefix, nameof(subscriptionNamePrefix), "Requires a non-blank subscription name of the Azure Service Bus Topic subscription, to receive Azure Key Vault events");
Guard.NotNullOrWhitespace(serviceBusTopicConnectionStringSecretKey, nameof(serviceBusTopicConnectionStringSecretKey), "Requires a non-blank secret key that points to a Azure Service Bus Topic");
Guard.NotNullOrWhitespace(messagePumpConnectionStringKey, nameof(messagePumpConnectionStringKey), "Requires a non-blank secret key that points to the credentials that holds the connection string of the target message pump");

services.AddCloudEventBackgroundJob(subscriptionNamePrefix, serviceBusTopicConnectionStringSecretKey, configureBackgroundJob)
.WithServiceBusMessageHandler<ReAuthenticateOnRotatedCredentialsMessageHandler, CloudEvent>(
messageBodyFilter: cloudEvent => cloudEvent?.GetPayload<SecretNewVersionCreated>() != null,
implementationFactory: serviceProvider =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
using Arcus.BackgroundJobs.Tests.Integration.Hosting.ServiceBus;
using Arcus.BackgroundJobs.Tests.Integration.KeyVault.Fixture;
using Arcus.EventGrid.Publishing;
using Arcus.Messaging.Pumps.ServiceBus;
using Arcus.Security.Providers.AzureKeyVault.Authentication;
using Arcus.Testing.Logging;
using Azure;
using Azure.Identity;
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Management.ServiceBus.Models;
Expand All @@ -23,6 +27,8 @@ namespace Arcus.BackgroundJobs.Tests.Integration.KeyVault
[Trait("Category", "Integration")]
public class AutoRestartServiceBusMessagePumpOnRotatedCredentialsJobTests
{
private const string ConnectionStringSecretKey = "ARCUS_KEYVAULT_SECRETNEWVERSIONCREATED_CONNECTIONSTRING";

private readonly ILogger _logger;

/// <summary>
Expand All @@ -43,7 +49,6 @@ public async Task ServiceBusMessagePump_RotateServiceBusConnectionKeysOnSecretNe
var applicationId = config.GetValue<string>("Arcus:ServicePrincipal:ApplicationId");
var clientKey = config.GetValue<string>("Arcus:ServicePrincipal:AccessKey");
var keyVaultUri = config.GetValue<string>("Arcus:KeyVault:Uri");

_logger.LogInformation("Using Service Principal [ClientID: '{0}']", applicationId);

var client = new ServiceBusConfiguration(rotationConfig, _logger);
Expand All @@ -52,19 +57,17 @@ public async Task ServiceBusMessagePump_RotateServiceBusConnectionKeysOnSecretNe
SecretClient keyVaultClient = CreateKeyVaultClient(tenantId, keyVaultUri, applicationId, clientKey);
await keyVaultClient.SetSecretAsync(rotationConfig.KeyVault.SecretName, freshConnectionString);

string jobId = Guid.NewGuid().ToString();
const string connectionStringSecretKey = "ARCUS_KEYVAULT_SECRETNEWVERSIONCREATED_CONNECTIONSTRING";

var jobId = Guid.NewGuid().ToString();
var options = new WorkerOptions();
options.Configuration.Add(connectionStringSecretKey, rotationConfig.KeyVault.SecretNewVersionCreated.ConnectionString);
options.Configuration.Add(ConnectionStringSecretKey, rotationConfig.KeyVault.SecretNewVersionCreated.ConnectionString);
AddEventGridPublisher(options, config);
AddSecretStore(options, tenantId, keyVaultUri, applicationId, clientKey);
AddServiceBusMessagePump(options, rotationConfig, jobId);

options.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
jobId: jobId,
subscriptionNamePrefix: "TestSub",
serviceBusTopicConnectionStringSecretKey: connectionStringSecretKey,
serviceBusTopicConnectionStringSecretKey: ConnectionStringSecretKey,
messagePumpConnectionStringKey: rotationConfig.KeyVault.SecretName);

await using (var worker = await Worker.StartNewAsync(options))
Expand All @@ -83,6 +86,58 @@ public async Task ServiceBusMessagePump_RotateServiceBusConnectionKeysOnSecretNe
}
}

[Theory]
[InlineData(TopicSubscription.None, false)]
[InlineData(TopicSubscription.CreateOnStart, true)]
[InlineData(TopicSubscription.DeleteOnStop, false)]
[InlineData(TopicSubscription.CreateOnStart | TopicSubscription.DeleteOnStop, true)]
public async Task AutoRestartServiceBusMessagePump_WithTopicSubscriptionInOptions_UsesOptions(
TopicSubscription topicSubscription,
bool expected)
{
var config = TestConfig.Create();
KeyRotationConfig rotationConfig = config.GetKeyRotationConfig();
var options = new WorkerOptions();
options.Configuration.Add(ConnectionStringSecretKey, rotationConfig.KeyVault.SecretNewVersionCreated.ConnectionString);

var tenantId = config.GetValue<string>("Arcus:KeyRotation:ServiceBus:TenantId");
var applicationId = config.GetValue<string>("Arcus:ServicePrincipal:ApplicationId");
var clientKey = config.GetValue<string>("Arcus:ServicePrincipal:AccessKey");
var keyVaultUri = config.GetValue<string>("Arcus:KeyVault:Uri");
var jobId = Guid.NewGuid().ToString();

AddSecretStore(options, tenantId, keyVaultUri, applicationId, clientKey);

var subscriptionPrefix = "AutoRestart";
options.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
jobId: jobId,
subscriptionNamePrefix: subscriptionPrefix,
serviceBusTopicConnectionStringSecretKey: ConnectionStringSecretKey,
messagePumpConnectionStringKey: rotationConfig.KeyVault.SecretName,
opt => opt.TopicSubscription = topicSubscription);

// Act
await using (var worker = await Worker.StartNewAsync(options))
{
// Assert
var client = new ServiceBusAdministrationClient(rotationConfig.KeyVault.SecretNewVersionCreated.ConnectionString);
var properties = ServiceBusConnectionStringProperties.Parse(rotationConfig.KeyVault.SecretNewVersionCreated.ConnectionString);

bool actual = false;
AsyncPageable<SubscriptionProperties> subscriptions = client.GetSubscriptionsAsync(properties.EntityPath);
await foreach (SubscriptionProperties sub in subscriptions)
{
if (sub.SubscriptionName.StartsWith(subscriptionPrefix))
{
await client.DeleteSubscriptionAsync(properties.EntityPath, sub.SubscriptionName);
actual = true;
}
}

Assert.True(expected == actual, $"Azure Service Bus topic subscription was not created/deleted as expected {expected} != {actual} when topic subscription '{topicSubscription}'");
}
}

private static SecretClient CreateKeyVaultClient(string tenantId, string keyVaultUri, string applicationId, string clientKey)
{
var credential = new ClientSecretCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,75 @@ public void AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob
"<service-bus-connection-string-secret-key>",
messagePumpConnectionStringKey));
}

[Theory]
[ClassData(typeof(Blanks))]
public void AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJobOptions_WithoutJobId_Fails(string jobId)
{
// Arrange
var services = new ServiceCollection();

// Act / Assert
Assert.ThrowsAny<ArgumentException>(() =>
services.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
jobId,
"<topic-subscription-prefix>",
"<service-bus-connection-string-secret-key>",
"<message-pump-connection-string-key>",
configureBackgroundJob: null));
}

[Theory]
[ClassData(typeof(Blanks))]
public void AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJobOptions_WithoutTopicSubscriptionPrefix_Fails(string topicSubscriptionPrefix)
{
// Arrange
var services = new ServiceCollection();

// Act / Assert
Assert.ThrowsAny<ArgumentException>(() =>
services.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
"<job-id>",
topicSubscriptionPrefix,
"<service-bus-connection-string-secret-key>",
"<message-pump-connection-string-key>",
configureBackgroundJob: null));
}

[Theory]
[ClassData(typeof(Blanks))]
public void AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJobOptions_WithoutServiceBusConnectionStringSecretKey_Fails(
string serviceBusConnectionStringSecretKey)
{
// Arrange
var services = new ServiceCollection();

// Act / Assert
Assert.ThrowsAny<ArgumentException>(() =>
services.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
"<job-id>",
"<topic-subscription-prefix>",
serviceBusConnectionStringSecretKey,
"<message-pump-connection-string-key>",
configureBackgroundJob: null));
}

[Theory]
[ClassData(typeof(Blanks))]
public void AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJobOptions_WithoutMessagePumpConnectionStringKey_Fails(
string messagePumpConnectionStringKey)
{
// Arrange
var services = new ServiceCollection();

// Act / Assert
Assert.ThrowsAny<ArgumentException>(() =>
services.AddAutoRestartServiceBusMessagePumpOnRotatedCredentialsBackgroundJob(
"<job-id>",
"<topic-subscription-prefix>",
"<service-bus-connection-string-secret-key>",
messagePumpConnectionStringKey,
configureBackgroundJob: null));
}
}
}