-
- AuthJanitor
-
-
@@ -77,7 +69,7 @@
-
+
diff --git a/src/AuthJanitor.AspNet/AuthJanitor.AspNet.Core.csproj b/src/AuthJanitor.AspNet/AuthJanitor.AspNet.Core.csproj
index a55b9a8..d38d477 100644
--- a/src/AuthJanitor.AspNet/AuthJanitor.AspNet.Core.csproj
+++ b/src/AuthJanitor.AspNet/AuthJanitor.AspNet.Core.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/AuthJanitor.AspNet/MetaServices/TaskExecutionMetaService.cs b/src/AuthJanitor.AspNet/MetaServices/TaskExecutionMetaService.cs
index f407064..f407c09 100644
--- a/src/AuthJanitor.AspNet/MetaServices/TaskExecutionMetaService.cs
+++ b/src/AuthJanitor.AspNet/MetaServices/TaskExecutionMetaService.cs
@@ -1,205 +1,219 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
-using AuthJanitor.UI.Shared.Models;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using AuthJanitor.UI.Shared.Models;
using AuthJanitor.EventSinks;
using AuthJanitor.IdentityServices;
-using AuthJanitor.Integrations.DataStores;
-using AuthJanitor.Providers;
+using AuthJanitor.Integrations.DataStores;
+using AuthJanitor.Providers;
using AuthJanitor.SecureStorage;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace AuthJanitor.UI.Shared.MetaServices
-{
- public class TaskExecutionMetaService
- {
- private readonly IDataStore
_managedSecrets;
- private readonly IDataStore _rekeyingTasks;
- private readonly IDataStore _resources;
- private readonly ISecureStorage _secureStorageProvider;
-
- private readonly ProviderManagerService _providerManagerService;
-
- private readonly EventDispatcherMetaService _eventDispatcherMetaService;
- private readonly IIdentityService _identityService;
-
- public TaskExecutionMetaService(
- EventDispatcherMetaService eventDispatcherMetaService,
- IIdentityService identityService,
- ProviderManagerService providerManagerService,
- IDataStore managedSecrets,
- IDataStore rekeyingTasks,
- IDataStore resources,
- ISecureStorage secureStorageProvider)
- {
- _eventDispatcherMetaService = eventDispatcherMetaService;
- _identityService = identityService;
- _providerManagerService = providerManagerService;
- _managedSecrets = managedSecrets;
- _rekeyingTasks = rekeyingTasks;
- _resources = resources;
- _secureStorageProvider = secureStorageProvider;
- }
-
- public async Task CacheBackCredentialsForTaskIdAsync(Guid taskId, CancellationToken cancellationToken)
- {
- var task = await _rekeyingTasks.GetOne(taskId, cancellationToken);
- if (task == null)
- throw new KeyNotFoundException("Task not found");
-
- if (task.ConfirmationType != TaskConfirmationStrategies.AdminCachesSignOff)
- throw new InvalidOperationException("Task does not persist credentials");
-
- if (_secureStorageProvider == null)
- throw new NotSupportedException("Must register an ISecureStorageProvider");
-
- var credentialId = await _identityService.GetAccessTokenOnBehalfOfCurrentUserAsync()
- .ContinueWith(t => _secureStorageProvider.Persist(task.Expiry, t.Result))
- .Unwrap();
-
- task.PersistedCredentialId = credentialId;
- task.PersistedCredentialUser = _identityService.UserName;
-
- await _rekeyingTasks.Update(task, cancellationToken);
- }
-
- public async Task ExecuteTask(Guid taskId, CancellationToken cancellationToken)
- {
- // Prepare record
- var task = await _rekeyingTasks.GetOne(taskId, cancellationToken);
- task.RekeyingInProgress = true;
- var rekeyingAttemptLog = new RekeyingAttemptLogger();
- task.Attempts.Add(rekeyingAttemptLog);
- await _rekeyingTasks.Update(task, cancellationToken);
-
- var logUpdateCancellationTokenSource = new CancellationTokenSource();
- var logUpdateTask = Task.Run(async () =>
- {
- while (task.RekeyingInProgress)
- {
- await Task.Delay(15 * 1000);
- await _rekeyingTasks.Update(task, cancellationToken);
- }
- }, logUpdateCancellationTokenSource.Token);
-
- // Retrieve credentials for Task
- AccessTokenCredential credential = null;
- try
- {
- if (task.ConfirmationType == TaskConfirmationStrategies.AdminCachesSignOff)
- {
- if (task.PersistedCredentialId == default)
- throw new KeyNotFoundException("Cached sign-off is preferred but no credentials were persisted!");
-
- if (_secureStorageProvider == null)
- throw new NotSupportedException("Must register an ISecureStorageProvider");
-
- credential = await _secureStorageProvider.Retrieve(task.PersistedCredentialId);
- }
- else if (task.ConfirmationType == TaskConfirmationStrategies.AdminSignsOffJustInTime)
- credential = await _identityService.GetAccessTokenOnBehalfOfCurrentUserAsync();
- else if (task.ConfirmationType.UsesServicePrincipal())
- credential = await _identityService.GetAccessTokenForApplicationAsync();
- else
- throw new NotSupportedException("No Access Tokens could be generated for this Task!");
-
- if (credential == null || string.IsNullOrEmpty(credential.AccessToken))
- throw new InvalidOperationException("Access Token was found, but was blank or invalid");
- }
- catch (Exception ex)
- {
- await EmbedException(task, ex, cancellationToken, "Exception retrieving Access Token");
- await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
- return;
- }
-
- // Embed credential context in attempt log
- rekeyingAttemptLog.UserDisplayName = credential.Username;
- rekeyingAttemptLog.UserEmail = credential.Username;
- if (task.ConfirmationType.UsesOBOTokens())
- rekeyingAttemptLog.UserDisplayName = task.PersistedCredentialUser;
-
- // Retrieve targets
- var secret = await _managedSecrets.GetOne(task.ManagedSecretId, cancellationToken);
- rekeyingAttemptLog.LogInformation("Beginning rekeying of secret ID {SecretId}", task.ManagedSecretId);
- var resources = await _resources.Get(r => secret.ResourceIds.Contains(r.ObjectId), cancellationToken);
-
- // Execute rekeying workflow
- try
- {
- var providers = resources.Select(r => _providerManagerService.GetProviderInstance(
- r.ProviderType,
- r.ProviderConfiguration)).ToList();
-
- // Link in automation bindings from the outer flow
- providers.ForEach(p => p.Credential = credential);
-
- await _providerManagerService.ExecuteRekeyingWorkflow(rekeyingAttemptLog, secret.ValidPeriod, providers);
- }
- catch (Exception ex)
- {
- await EmbedException(task, ex, cancellationToken, "Error executing rekeying workflow!");
- await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
- }
-
- // Update Task record
- task.RekeyingInProgress = false;
- task.RekeyingCompleted = rekeyingAttemptLog.IsSuccessfulAttempt;
- task.RekeyingFailed = !rekeyingAttemptLog.IsSuccessfulAttempt;
-
- logUpdateCancellationTokenSource.Cancel();
-
- await _rekeyingTasks.Update(task, cancellationToken);
-
- // Run cleanup if Task is complete
- if (task.RekeyingCompleted)
- {
- try
- {
- secret.LastChanged = DateTimeOffset.UtcNow;
- await _managedSecrets.Update(secret, cancellationToken);
-
- if (task.PersistedCredentialId != default && task.PersistedCredentialId != Guid.Empty)
- {
- rekeyingAttemptLog.LogInformation("Destroying persisted credential");
- await _secureStorageProvider.Destroy(task.PersistedCredentialId);
-
- task.PersistedCredentialId = default;
- task.PersistedCredentialUser = default;
- }
-
- rekeyingAttemptLog.LogInformation("Completed rekeying workflow for ManagedSecret '{ManagedSecretName}' (ID {ManagedSecretId})", secret.Name, secret.ObjectId);
- rekeyingAttemptLog.LogInformation("Rekeying task completed");
-
- await _rekeyingTasks.Update(task, cancellationToken);
- }
- catch (Exception ex)
- {
- await EmbedException(task, ex, cancellationToken, "Error cleaning up after rekeying!");
- }
-
-
- if (task.ConfirmationType.UsesOBOTokens())
- await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskCompletedManually, nameof(TaskExecutionMetaService.ExecuteTask), task);
- else
- await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskCompletedAutomatically, nameof(TaskExecutionMetaService.ExecuteTask), task);
- }
- else
- await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
- }
-
- private async Task EmbedException(RekeyingTask task, Exception ex, CancellationToken cancellationToken, string text = "Exception Occurred")
- {
- var myAttempt = task.Attempts.OrderByDescending(a => a.AttemptStarted).First();
- if (text != default) myAttempt.LogCritical(ex, text);
- myAttempt.OuterException = $"{ex.Message}{Environment.NewLine}{ex.StackTrace}";
- task.RekeyingInProgress = false;
- task.RekeyingFailed = true;
- await _rekeyingTasks.Update(task, cancellationToken);
- }
- }
-}
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using System.Text.RegularExpressions;
+
+namespace AuthJanitor.UI.Shared.MetaServices
+{
+ public class TaskExecutionMetaService
+ {
+ private readonly IDataStore _managedSecrets;
+ private readonly IDataStore _rekeyingTasks;
+ private readonly IDataStore _resources;
+ private readonly ISecureStorage _secureStorageProvider;
+
+ private readonly ProviderManagerService _providerManagerService;
+
+ private readonly EventDispatcherMetaService _eventDispatcherMetaService;
+ private readonly IIdentityService _identityService;
+
+ public TaskExecutionMetaService(
+ EventDispatcherMetaService eventDispatcherMetaService,
+ IIdentityService identityService,
+ ProviderManagerService providerManagerService,
+ IDataStore managedSecrets,
+ IDataStore rekeyingTasks,
+ IDataStore resources,
+ ISecureStorage secureStorageProvider)
+ {
+ _eventDispatcherMetaService = eventDispatcherMetaService;
+ _identityService = identityService;
+ _providerManagerService = providerManagerService;
+ _managedSecrets = managedSecrets;
+ _rekeyingTasks = rekeyingTasks;
+ _resources = resources;
+ _secureStorageProvider = secureStorageProvider;
+ }
+
+ public async Task CacheBackCredentialsForTaskIdAsync(Guid taskId, CancellationToken cancellationToken)
+ {
+ var task = await _rekeyingTasks.GetOne(taskId, cancellationToken);
+ if (task == null)
+ throw new KeyNotFoundException("Task not found");
+
+ if (task.ConfirmationType != TaskConfirmationStrategies.AdminCachesSignOff)
+ throw new InvalidOperationException("Task does not persist credentials");
+
+ if (_secureStorageProvider == null)
+ throw new NotSupportedException("Must register an ISecureStorageProvider");
+
+ var credentialId = await _identityService.GetAccessTokenOnBehalfOfCurrentUserAsync()
+ .ContinueWith(t => _secureStorageProvider.Persist(task.Expiry, t.Result))
+ .Unwrap();
+
+ task.PersistedCredentialId = credentialId;
+ task.PersistedCredentialUser = _identityService.UserName;
+
+ await _rekeyingTasks.Update(task, cancellationToken);
+ }
+
+ public async Task ExecuteTask(Guid taskId, CancellationToken cancellationToken)
+ {
+ // Prepare record
+ var task = await _rekeyingTasks.GetOne(taskId, cancellationToken);
+ task.RekeyingInProgress = true;
+ var rekeyingAttemptLog = new RekeyingAttemptLogger();
+ task.Attempts.Add(rekeyingAttemptLog);
+ await _rekeyingTasks.Update(task, cancellationToken);
+
+ var logUpdateCancellationTokenSource = new CancellationTokenSource();
+ var logUpdateTask = Task.Run(async () =>
+ {
+ while (task.RekeyingInProgress)
+ {
+ await Task.Delay(15 * 1000);
+ await _rekeyingTasks.Update(task, cancellationToken);
+ }
+ }, logUpdateCancellationTokenSource.Token);
+
+ // Retrieve credentials for Task
+ AccessTokenCredential credential = null;
+ try
+ {
+ if (task.ConfirmationType == TaskConfirmationStrategies.AdminCachesSignOff)
+ {
+ if (task.PersistedCredentialId == default)
+ throw new KeyNotFoundException("Cached sign-off is preferred but no credentials were persisted!");
+
+ if (_secureStorageProvider == null)
+ throw new NotSupportedException("Must register an ISecureStorageProvider");
+
+ credential = await _secureStorageProvider.Retrieve(task.PersistedCredentialId);
+ }
+ else if (task.ConfirmationType == TaskConfirmationStrategies.AdminSignsOffJustInTime)
+ credential = await _identityService.GetAccessTokenOnBehalfOfCurrentUserAsync();
+ else if (task.ConfirmationType.UsesServicePrincipal())
+ credential = await _identityService.GetAccessTokenForApplicationAsync();
+ else
+ throw new NotSupportedException("No Access Tokens could be generated for this Task!");
+
+ if (credential == null || string.IsNullOrEmpty(credential.AccessToken))
+ throw new InvalidOperationException("Access Token was found, but was blank or invalid");
+ }
+ catch (Exception ex)
+ {
+ await EmbedException(task, ex, cancellationToken, "Exception retrieving Access Token");
+ await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
+ return;
+ }
+
+ // Embed credential context in attempt log
+ rekeyingAttemptLog.UserDisplayName = credential.Username;
+ rekeyingAttemptLog.UserEmail = credential.Username;
+ if (task.ConfirmationType.UsesOBOTokens())
+ {
+ if (!string.IsNullOrEmpty(task.PersistedCredentialUser))
+ rekeyingAttemptLog.UserDisplayName = task.PersistedCredentialUser;
+ else
+ {
+ rekeyingAttemptLog.UserDisplayName = _identityService.UserName;
+ rekeyingAttemptLog.UserEmail = _identityService.UserEmail;
+ }
+ }
+
+ // Retrieve targets
+ var secret = await _managedSecrets.GetOne(task.ManagedSecretId, cancellationToken);
+ rekeyingAttemptLog.LogInformation("Beginning rekeying of secret ID {SecretId}", task.ManagedSecretId);
+ var resources = await _resources.Get(r => secret.ResourceIds.Contains(r.ObjectId), cancellationToken);
+
+ await _rekeyingTasks.Update(task, cancellationToken);
+
+ // Execute rekeying workflow
+ try
+ {
+ var providers = resources.Select(r => _providerManagerService.GetProviderInstance(
+ r.ProviderType,
+ r.ProviderConfiguration)).ToList();
+
+ // Link in automation bindings from the outer flow
+ providers.ForEach(p => p.Credential = credential);
+
+ await _providerManagerService.ExecuteRekeyingWorkflow(rekeyingAttemptLog, secret.ValidPeriod, providers);
+ }
+ catch (Exception ex)
+ {
+ rekeyingAttemptLog.IsComplete = true;
+ await EmbedException(task, ex, cancellationToken, "Error executing rekeying workflow!");
+ await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
+ }
+
+ // Update Task record
+ task.RekeyingInProgress = false;
+ task.RekeyingCompleted = rekeyingAttemptLog.IsSuccessfulAttempt;
+ task.RekeyingFailed = !rekeyingAttemptLog.IsSuccessfulAttempt;
+
+ logUpdateCancellationTokenSource.Cancel();
+
+ await _rekeyingTasks.Update(task, cancellationToken);
+
+ // Run cleanup if Task is complete
+ if (task.RekeyingCompleted)
+ {
+ try
+ {
+ secret.LastChanged = DateTimeOffset.UtcNow;
+ await _managedSecrets.Update(secret, cancellationToken);
+
+ if (task.PersistedCredentialId != default && task.PersistedCredentialId != Guid.Empty)
+ {
+ rekeyingAttemptLog.LogInformation("Destroying persisted credential");
+ await _secureStorageProvider.Destroy(task.PersistedCredentialId);
+
+ task.PersistedCredentialId = default;
+ task.PersistedCredentialUser = default;
+ }
+
+ rekeyingAttemptLog.LogInformation("Completed rekeying workflow for ManagedSecret '{ManagedSecretName}' (ID {ManagedSecretId})", secret.Name, secret.ObjectId);
+ rekeyingAttemptLog.LogInformation("Rekeying task completed");
+
+ await _rekeyingTasks.Update(task, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ await EmbedException(task, ex, cancellationToken, "Error cleaning up after rekeying!");
+ }
+
+
+ if (task.ConfirmationType.UsesOBOTokens())
+ await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskCompletedManually, nameof(TaskExecutionMetaService.ExecuteTask), task);
+ else
+ await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskCompletedAutomatically, nameof(TaskExecutionMetaService.ExecuteTask), task);
+ }
+ else
+ await _eventDispatcherMetaService.DispatchEvent(AuthJanitorSystemEvents.RotationTaskAttemptFailed, nameof(TaskExecutionMetaService.ExecuteTask), task);
+ }
+
+ private async Task EmbedException(RekeyingTask task, Exception ex, CancellationToken cancellationToken, string text = "Exception Occurred")
+ {
+ var myAttempt = task.Attempts.OrderByDescending(a => a.AttemptStarted).First();
+ if (text != default) myAttempt.LogCritical(ex, text);
+ myAttempt.OuterException = Regex.Replace(JsonConvert.SerializeObject(ex, Formatting.Indented), "Bearer [A-Za-z0-9\\-\\._~\\+\\/]+=*", "<>");
+ //myAttempt.OuterException = $"{ex.Message}{Environment.NewLine}{ex.StackTrace}";
+ task.RekeyingInProgress = false;
+ task.RekeyingFailed = true;
+ await _rekeyingTasks.Update(task, cancellationToken);
+ }
+ }
+}
diff --git a/src/AuthJanitor.AspNet/ViewModelFactory.cs b/src/AuthJanitor.AspNet/ViewModelFactory.cs
index 322f48a..ff32589 100644
--- a/src/AuthJanitor.AspNet/ViewModelFactory.cs
+++ b/src/AuthJanitor.AspNet/ViewModelFactory.cs
@@ -13,6 +13,7 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using AuthJanitor.IdentityServices;
+using AuthJanitor.Providers.Capabilities;
namespace AuthJanitor.UI.Shared
{
@@ -95,9 +96,31 @@ namespace AuthJanitor.UI.Shared
IsRekeyableObjectProvider = provider.IsRekeyableObjectProvider,
OriginatingFile = Path.GetFileName(provider.OriginatingFile),
ProviderTypeName = provider.ProviderTypeName,
- SvgImage = provider.SvgImage
+ Capabilities = GetProviderCapabilities(provider.ProviderType)
};
+ private static IEnumerable GetProviderCapabilities(Type providerType)
+ {
+ var capabilities = new List();
+ if (typeof(ICanEnumerateResourceCandidates).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanEnumerateResourceCandidates);
+ if (typeof(ICanRunSanityTests).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanRunSanityTests);
+ if (typeof(ICanCleanup).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanCleanup);
+ if (typeof(ICanDistributeTemporarySecretValues).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanDistributeTemporarySecrets);
+ if (typeof(ICanGenerateTemporarySecretValue).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanGenerateTemporarySecrets);
+ if (typeof(ICanPerformUnifiedCommit).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanPerformUnifiedCommits);
+ if (typeof(ICanPerformUnifiedCommitForTemporarySecretValues).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanPerformUnifiedCommitForTemporarySecret);
+ if (typeof(ICanCleanup).IsAssignableFrom(providerType))
+ capabilities.Add(ProviderCapabilities.CanCleanup);
+ return capabilities;
+ }
+
private static ManagedSecretViewModel GetViewModel(IServiceProvider serviceProvider, ManagedSecret secret, CancellationToken cancellationToken)
{
var providerManagerService = serviceProvider.GetRequiredService();
diff --git a/src/AuthJanitor.AspNet/ViewModels/LoadedProviderViewModel.cs b/src/AuthJanitor.AspNet/ViewModels/LoadedProviderViewModel.cs
index e024eed..5d0f8ea 100644
--- a/src/AuthJanitor.AspNet/ViewModels/LoadedProviderViewModel.cs
+++ b/src/AuthJanitor.AspNet/ViewModels/LoadedProviderViewModel.cs
@@ -1,17 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System.Collections.Generic;
using AuthJanitor.Providers;
namespace AuthJanitor.UI.Shared.ViewModels
{
+ public enum ProviderCapabilities
+ {
+ None,
+ CanCleanup,
+ CanDistributeTemporarySecrets,
+ CanEnumerateResourceCandidates,
+ CanGenerateTemporarySecrets,
+ CanPerformUnifiedCommits,
+ CanPerformUnifiedCommitForTemporarySecret,
+ CanRunSanityTests
+ }
+
public class LoadedProviderViewModel : IAuthJanitorViewModel
{
public string OriginatingFile { get; set; }
public string ProviderTypeName { get; set; }
public bool IsRekeyableObjectProvider { get; set; }
public ProviderAttribute Details { get; set; }
- public string SvgImage { get; set; }
-
+ public IEnumerable Capabilities { get; set; } = new[] { ProviderCapabilities.None };
public string AssemblyVersion { get; set; }
}
}
diff --git a/src/AuthJanitor.AspNet/ViewModels/ProviderConfigurationViewModel.cs b/src/AuthJanitor.AspNet/ViewModels/ProviderConfigurationViewModel.cs
index c83e39e..a6fafde 100644
--- a/src/AuthJanitor.AspNet/ViewModels/ProviderConfigurationViewModel.cs
+++ b/src/AuthJanitor.AspNet/ViewModels/ProviderConfigurationViewModel.cs
@@ -45,7 +45,6 @@ namespace AuthJanitor.UI.Shared.ViewModels
var deserialized = JsonSerializer.Deserialize>(value);
foreach (var item in ConfigurationItems)
{
- System.Console.WriteLine($"{item.Name} => {deserialized[item.Name]} ({item.InputType})");
if (deserialized.ContainsKey(item.Name))
{
switch (item.InputType)
diff --git a/src/AuthJanitor.Core/AuthJanitor.Core.csproj b/src/AuthJanitor.Core/AuthJanitor.Core.csproj
index e96eeca..2b4af7d 100644
--- a/src/AuthJanitor.Core/AuthJanitor.Core.csproj
+++ b/src/AuthJanitor.Core/AuthJanitor.Core.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/AuthJanitor.Core/Providers/ApplicationLifecycleProvider.cs b/src/AuthJanitor.Core/Providers/ApplicationLifecycleProvider.cs
index 3561afc..eaa8f4a 100644
--- a/src/AuthJanitor.Core/Providers/ApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Core/Providers/ApplicationLifecycleProvider.cs
@@ -10,21 +10,10 @@ namespace AuthJanitor.Providers
///
public interface IApplicationLifecycleProvider : IAuthJanitorProvider
{
- ///
- /// Call to prepare the application for a new secret, passing in a secret
- /// which will be valid while the Rekeying is taking place (for zero-downtime)
- ///
- Task BeforeRekeying(List temporaryUseSecrets);
-
///
/// Call to commit the newly generated secret(s)
///
- Task CommitNewSecrets(List newSecrets);
-
- ///
- /// Call after all new keys have been committed
- ///
- Task AfterRekeying();
+ Task DistributeLongTermSecretValues(List newSecretValues);
}
///
@@ -33,26 +22,9 @@ namespace AuthJanitor.Providers
public abstract class ApplicationLifecycleProvider : AuthJanitorProvider, IApplicationLifecycleProvider
where TProviderConfiguration : AuthJanitorProviderConfiguration
{
- ///
- /// Call to prepare the application for a new secret, passing in a secret
- /// which will be valid while the Rekeying is taking place (for zero-downtime)
- ///
- public virtual Task BeforeRekeying(List temporaryUseSecrets)
- {
- return Task.FromResult(true);
- }
-
///
/// Call to commit the newly generated secret(s)
///
- public abstract Task CommitNewSecrets(List newSecrets);
-
- ///
- /// Call after all new keys have been committed
- ///
- public virtual Task AfterRekeying()
- {
- return Task.FromResult(true);
- }
+ public abstract Task DistributeLongTermSecretValues(List newSecretValues);
}
}
diff --git a/src/AuthJanitor.Core/Providers/AuthJanitorProvider.cs b/src/AuthJanitor.Core/Providers/AuthJanitorProvider.cs
index f375c24..780b599 100644
--- a/src/AuthJanitor.Core/Providers/AuthJanitorProvider.cs
+++ b/src/AuthJanitor.Core/Providers/AuthJanitorProvider.cs
@@ -1,11 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
-using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
-using System.Threading.Tasks;
namespace AuthJanitor.Providers
{
@@ -21,12 +19,6 @@ namespace AuthJanitor.Providers
///
AccessTokenCredential Credential { get; set; }
- ///
- /// Test if the current credentials can execute an Extension
- ///
- ///
- Task Test();
-
///
/// Get a text description of the action which is taken by the Extension
///
@@ -50,6 +42,8 @@ namespace AuthJanitor.Providers
/// Get the Provider's metadata
///
ProviderAttribute ProviderMetadata => GetType().GetCustomAttribute();
+
+ int GenerateResourceIdentifierHashCode();
}
///
@@ -77,15 +71,6 @@ namespace AuthJanitor.Providers
///
public AccessTokenCredential Credential { get; set; }
- ///
- /// Test if the current credentials can execute an Extension
- ///
- ///
- public virtual Task Test()
- {
- return Task.FromResult(true);
- }
-
///
/// Get a text description of the action which is taken by the Provider
///
@@ -104,5 +89,7 @@ namespace AuthJanitor.Providers
///
///
public virtual IList GetRisks() => new List();
+
+ public int GenerateResourceIdentifierHashCode() => Configuration.GenerateResourceIdentifierHashCode();
}
}
diff --git a/src/AuthJanitor.Core/Providers/AuthJanitorProviderConfiguration.cs b/src/AuthJanitor.Core/Providers/AuthJanitorProviderConfiguration.cs
index 0eebba7..a6d77bd 100644
--- a/src/AuthJanitor.Core/Providers/AuthJanitorProviderConfiguration.cs
+++ b/src/AuthJanitor.Core/Providers/AuthJanitorProviderConfiguration.cs
@@ -12,5 +12,12 @@ namespace AuthJanitor.Providers
/// RegeneratedSecrets entering an ApplicationLifecycleProvider
///
public string UserHint { get; set; }
+
+ ///
+ /// Creates a HashCode for this configuration based on common resources.
+ /// This is used to group items for parallelization and unified commits.
+ /// For example, this might be a ResourceGroup/ResourceName.
+ ///
+ public abstract int GenerateResourceIdentifierHashCode();
}
}
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/IAuthJanitorCapability.cs b/src/AuthJanitor.Core/Providers/Capabilities/IAuthJanitorCapability.cs
new file mode 100644
index 0000000..3764c4d
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/IAuthJanitorCapability.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface IAuthJanitorCapability : IAuthJanitorProvider { }
+}
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanCleanup.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanCleanup.cs
new file mode 100644
index 0000000..8e1f9db
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanCleanup.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanCleanup : IAuthJanitorCapability
+ {
+ Task Cleanup();
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanDistributeTemporarySecretValues.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanDistributeTemporarySecretValues.cs
new file mode 100644
index 0000000..10229cd
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanDistributeTemporarySecretValues.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanDistributeTemporarySecretValues : IAuthJanitorCapability
+ {
+ Task DistributeTemporarySecretValues(List secretValues);
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanEnumerateResourceCandidates.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanEnumerateResourceCandidates.cs
new file mode 100644
index 0000000..931913c
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanEnumerateResourceCandidates.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using AuthJanitor.Providers;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanEnumerateResourceCandidates : IAuthJanitorCapability
+ {
+ Task> EnumerateResourceCandidates(AuthJanitorProviderConfiguration baseConfig);
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanGenerateTemporarySecretValue.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanGenerateTemporarySecretValue.cs
new file mode 100644
index 0000000..19fa4a5
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanGenerateTemporarySecretValue.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanGenerateTemporarySecretValue : IAuthJanitorCapability
+ {
+ Task GenerateTemporarySecretValue();
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommit.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommit.cs
new file mode 100644
index 0000000..2d15739
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommit.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanPerformUnifiedCommit : IAuthJanitorCapability
+ {
+ Task UnifiedCommit();
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommitForTemporarySecretValues.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommitForTemporarySecretValues.cs
new file mode 100644
index 0000000..fe02279
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommitForTemporarySecretValues.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanPerformUnifiedCommitForTemporarySecretValues : IAuthJanitorCapability
+ {
+ Task UnifiedCommitForTemporarySecretValues();
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/Capabilities/ICanRunSanityTests.cs b/src/AuthJanitor.Core/Providers/Capabilities/ICanRunSanityTests.cs
new file mode 100644
index 0000000..9175b03
--- /dev/null
+++ b/src/AuthJanitor.Core/Providers/Capabilities/ICanRunSanityTests.cs
@@ -0,0 +1,11 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+using System.Threading.Tasks;
+
+namespace AuthJanitor.Providers.Capabilities
+{
+ public interface ICanRunSanityTests : IAuthJanitorCapability
+ {
+ Task Test();
+ }
+}
\ No newline at end of file
diff --git a/src/AuthJanitor.Core/Providers/LoadedProviderMetadata.cs b/src/AuthJanitor.Core/Providers/LoadedProviderMetadata.cs
index 83c8f69..d8625d5 100644
--- a/src/AuthJanitor.Core/Providers/LoadedProviderMetadata.cs
+++ b/src/AuthJanitor.Core/Providers/LoadedProviderMetadata.cs
@@ -37,11 +37,6 @@ namespace AuthJanitor.Providers
///
public ProviderAttribute Details { get; set; }
- ///
- /// Provider SVG logo image
- ///
- public string SvgImage { get; set; }
-
///
/// NET Assembly where the Provider was loaded from
///
diff --git a/src/AuthJanitor.Core/Providers/ProviderAttribute.cs b/src/AuthJanitor.Core/Providers/ProviderAttribute.cs
index e848fb7..86a3fbc 100644
--- a/src/AuthJanitor.Core/Providers/ProviderAttribute.cs
+++ b/src/AuthJanitor.Core/Providers/ProviderAttribute.cs
@@ -4,17 +4,6 @@ using System;
namespace AuthJanitor.Providers
{
- [Flags]
- public enum ProviderFeatureFlags : int
- {
- None = 0b00000000,
-
- IsTestable = 0b00000010,
- CanRotateWithoutDowntime = 0b00000100,
- SupportsSecondaryKey = 0b00001000,
- HasCandidateSelection = 0b00010000
- }
-
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ProviderAttribute : Attribute
{
@@ -28,21 +17,9 @@ namespace AuthJanitor.Providers
///
public string Description { get; set; }
- ///
- /// Features supported by this Provider
- ///
- public ProviderFeatureFlags Features { get; set; }
- }
-
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
- public class ProviderImageAttribute : Attribute
- {
///
/// SVG logo image for Provider
///
public string SvgImage { get; set; }
-
- public ProviderImageAttribute(string svgImage) =>
- SvgImage = svgImage;
}
}
diff --git a/src/AuthJanitor.Core/Providers/ProviderManagerService.cs b/src/AuthJanitor.Core/Providers/ProviderManagerService.cs
index d0f0d87..9a1bd9a 100644
--- a/src/AuthJanitor.Core/Providers/ProviderManagerService.cs
+++ b/src/AuthJanitor.Core/Providers/ProviderManagerService.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
@@ -13,9 +14,9 @@ using System.Threading.Tasks;
namespace AuthJanitor.Providers
{
-
public class ProviderManagerService
{
+ private const int DELAY_BETWEEN_ACTIONS_MS = 1000;
public static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions()
{
WriteIndented = false,
@@ -40,8 +41,7 @@ namespace AuthJanitor.Providers
ProviderTypeName = type.AssemblyQualifiedName,
ProviderType = type,
ProviderConfigurationType = type.BaseType.GetGenericArguments()[0],
- Details = type.GetCustomAttribute(),
- SvgImage = type.GetCustomAttribute()?.SvgImage
+ Details = type.GetCustomAttribute()
})
.ToList()
.AsReadOnly();
@@ -97,63 +97,72 @@ namespace AuthJanitor.Providers
IEnumerable providers)
{
logger.LogInformation("########## BEGIN REKEYING WORKFLOW ##########");
- var rkoProviders = providers.OfType().ToList();
- var alcProviders = providers.OfType().ToList();
-
- // NOTE: avoid costs of generating list of providers if information logging not turned on
- if (logger.IsEnabled(LogLevel.Information))
- {
- logger.LogInformation("RKO: {ProviderTypeNames}", string.Join(", ", rkoProviders.Select(p => p.GetType().Name)));
- logger.LogInformation("ALC: {ProviderTypeNames}", string.Join(", ", alcProviders.Select(p => p.GetType().Name)));
- }
// -----
- logger.LogInformation("### Performing Provider Tests.");
+ logger.LogInformation("### Performing provider tests...");
- await PerformProviderActions(
- logger,
- providers,
+ await PerformActionsInParallel(
+ logger,
+ providers.OfType(),
p => p.Test(),
"Error running sanity test on provider '{ProviderName}'",
"Error running one or more sanity tests!");
- logger.LogInformation("### Retrieving/generating temporary secrets.");
+ // -----
+
+ logger.LogInformation("### Retrieving/generating temporary secrets...");
var temporarySecrets = new List();
- await PerformProviderActions(
+ await PerformActionsInParallel(
logger,
- rkoProviders,
- p => p.GetSecretToUseDuringRekeying()
- .ContinueWith(t =>
- {
- if (t.Result != null)
- {
- temporarySecrets.Add(t.Result);
- }
- }),
+ providers.OfType(),
+ p => p.GenerateTemporarySecretValue()
+ .ContinueWith(t =>
+ {
+ if (t.Result != null)
+ {
+ temporarySecrets.Add(t.Result);
+ }
+ }),
"Error getting temporary secret from provider '{ProviderName}'",
"Error retrieving temporary secrets from one or more Rekeyable Object Providers!");
logger.LogInformation("{SecretCount} temporary secrets were created/read to be used during operation.", temporarySecrets.Count);
- // ---
+ // -----
- logger.LogInformation("### Preparing {ProviderCount} Application Lifecycle Providers for rekeying...", alcProviders.Count);
- await PerformProviderActions(
+ logger.LogInformation("### Distributing temporary secrets...");
+
+ await PerformActionsInParallelGroups(
logger,
- alcProviders,
- p => p.BeforeRekeying(temporarySecrets),
- "Error preparing ALC provider '{ProviderName}'",
- "Error preparing one or more Application Lifecycle Providers for rekeying!");
+ providers.OfType()
+ .GroupBy(p => p.GenerateResourceIdentifierHashCode()),
+ p => p.DistributeTemporarySecretValues(temporarySecrets),
+ "Error distributing secrets to ALC provider '{ProviderName}'",
+ "Error distributing secrets!");
// -----
- logger.LogInformation("### Rekeying {ProviderCount} Rekeyable Object Providers...", rkoProviders.Count);
- var newSecrets = new List();
- await PerformProviderActions(
+ logger.LogInformation("### Performing commits for temporary secrets...");
+
+ await PerformActionsInParallel(
logger,
- rkoProviders,
+ providers.OfType()
+ .GroupBy(p => p.GenerateResourceIdentifierHashCode())
+ .Select(g => g.First()),
+ p => p.UnifiedCommitForTemporarySecretValues(),
+ "Error committing temporary secrets for ALC provider '{ProviderName}'",
+ "Error committing temporary secrets!");
+
+ // -----
+
+ logger.LogInformation("### Rekeying objects and services...");
+
+ var newSecrets = new List();
+ await PerformActionsInParallel(
+ logger,
+ providers.OfType(),
p => p.Rekey(validPeriod)
.ContinueWith(t =>
{
@@ -169,43 +178,104 @@ namespace AuthJanitor.Providers
// -----
- logger.LogInformation("### Committing {SecretCount} regenerated secrets to {ProviderCount} Application Lifecycle Providers...",
- newSecrets.Count,
- alcProviders.Count);
+ logger.LogInformation("### Distributing regenerated secrets...");
- await PerformProviderActions(
+ await PerformActionsInParallelGroups(
logger,
- alcProviders,
- p => p.CommitNewSecrets(newSecrets),
+ providers.OfType()
+ .GroupBy(p => p.GenerateResourceIdentifierHashCode()),
+ p => p.DistributeLongTermSecretValues(newSecrets),
"Error committing to provider '{ProviderName}'",
"Error committing regenerated secrets!");
-
- // -----
-
- logger.LogInformation("### Completing post-rekey operations on Application Lifecycle Providers...");
-
- await PerformProviderActions(
- logger,
- alcProviders,
- p => p.AfterRekeying(),
- "Error running post-rekey operations on provider '{ProviderName}'",
- "Error running post-rekey operations on one or more Application Lifecycle Providers!");
// -----
- logger.LogInformation("### Completing finalizing operations on Rekeyable Object Providers...");
+ logger.LogInformation("### Performing commits...");
- await PerformProviderActions(
+ await PerformActionsInParallel(
logger,
- rkoProviders,
- p => p.OnConsumingApplicationSwapped(),
- "Error running after-swap operations on provider '{ProviderName}'",
- "Error running after-swap operations on one or more Rekeyable Object Providers!");
+ providers.OfType()
+ .GroupBy(p => p.GenerateResourceIdentifierHashCode())
+ .Select(g => g.First()),
+ p => p.UnifiedCommit(),
+ "Error committing secrets for ALC provider '{ProviderName}'",
+ "Error committing secrets!");
+
+ // -----
+
+ logger.LogInformation("### Running cleanup operations...");
+
+ await PerformActionsInParallelGroups(
+ logger,
+ providers.OfType()
+ .GroupBy(p => p.GenerateResourceIdentifierHashCode()),
+ p => p.Cleanup(),
+ "Error cleaning up provider '{ProviderName}'",
+ "Error cleaning up!");
logger.LogInformation("########## END REKEYING WORKFLOW ##########");
+
+ logger.IsComplete = true;
}
- private static async Task PerformProviderActions(
+ private static async Task PerformActionsInSerial(
+ ILogger logger,
+ IEnumerable providers,
+ Func providerAction,
+ string individualFailureErrorLogMessageTemplate)
+ where TProviderType : IAuthJanitorProvider
+ {
+ foreach (var provider in providers)
+ {
+ try
+ {
+ logger.LogInformation("Running action in serial on {0}", provider.GetType().Name);
+ await providerAction(provider);
+ await Task.Delay(DELAY_BETWEEN_ACTIONS_MS / 2);
+ }
+ catch (Exception exception)
+ {
+ logger.LogError(exception, individualFailureErrorLogMessageTemplate, provider.GetType().Name);
+
+ throw;
+ }
+ }
+ }
+
+ private static async Task PerformActionsInParallelGroups(
+ ILogger logger,
+ IEnumerable> providers,
+ Func providerAction,
+ string individualFailureErrorLogMessageTemplate,
+ string anyFailureExceptionMessage)
+ where TProviderType : IAuthJanitorProvider
+ {
+ var providerActions = providers.Select(async p =>
+ {
+ await PerformActionsInSerial(
+ logger,
+ p,
+ providerAction,
+ individualFailureErrorLogMessageTemplate);
+ });
+
+ try
+ {
+ await Task.WhenAll(providerActions);
+ }
+ catch (Exception exception)
+ {
+ throw new Exception(anyFailureExceptionMessage, exception);
+ }
+
+ if (providers.Any())
+ {
+ logger.LogInformation("Sleeping for {SleepTime}", DELAY_BETWEEN_ACTIONS_MS);
+ await Task.Delay(DELAY_BETWEEN_ACTIONS_MS);
+ }
+ }
+
+ private static async Task PerformActionsInParallel(
ILogger logger,
IEnumerable providers,
Func providerAction,
@@ -217,6 +287,7 @@ namespace AuthJanitor.Providers
{
try
{
+ logger.LogInformation("Running action in parallel on {0}", p.GetType().Name);
await providerAction(p);
}
catch (Exception exception)
@@ -235,6 +306,12 @@ namespace AuthJanitor.Providers
{
throw new Exception(anyFailureExceptionMessage, exception);
}
+
+ if (providers.Any())
+ {
+ logger.LogInformation("Sleeping for {SleepTime}", DELAY_BETWEEN_ACTIONS_MS);
+ await Task.Delay(DELAY_BETWEEN_ACTIONS_MS);
+ }
}
}
}
diff --git a/src/AuthJanitor.Core/Providers/RekeyableObjectProvider.cs b/src/AuthJanitor.Core/Providers/RekeyableObjectProvider.cs
index b1a6474..7673f0e 100644
--- a/src/AuthJanitor.Core/Providers/RekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Core/Providers/RekeyableObjectProvider.cs
@@ -7,25 +7,12 @@ namespace AuthJanitor.Providers
{
public interface IRekeyableObjectProvider : IAuthJanitorProvider
{
- ///
- /// Call before Rekeying occurs to get a secondary secret which will continue
- /// to work while Rekeying is taking place (if any).
- ///
- ///
- Task GetSecretToUseDuringRekeying();
-
///
/// Call when ready to rekey a given RekeyableService.
///
/// Requested period of validity for new key/secret
///
Task Rekey(TimeSpan requestedValidPeriod);
-
- ///
- /// Call when the ConsumingApplication has been moved to the RegeneratedKey (from Rekey())
- ///
- ///
- Task OnConsumingApplicationSwapped();
}
///
@@ -33,30 +20,11 @@ namespace AuthJanitor.Providers
///
public abstract class RekeyableObjectProvider : AuthJanitorProvider, IRekeyableObjectProvider where TConfiguration : AuthJanitorProviderConfiguration
{
- ///
- /// Call before Rekeying occurs to get a secondary secret which will continue
- /// to work while Rekeying is taking place (if any).
- ///
- public virtual async Task GetSecretToUseDuringRekeying()
- {
- await Task.Yield();
- return null;
- }
-
///
/// Call when ready to rekey a given RekeyableService.
///
/// Requested period of validity for new key/secret
///
public abstract Task Rekey(TimeSpan requestedValidPeriod);
-
- ///
- /// Call when the ConsumingApplication has been moved to the RegeneratedKey (from Rekey())
- ///
- ///
- public virtual Task OnConsumingApplicationSwapped()
- {
- return Task.FromResult(true);
- }
}
}
diff --git a/src/AuthJanitor.Core/Providers/RekeyingAttemptLogger.cs b/src/AuthJanitor.Core/Providers/RekeyingAttemptLogger.cs
index 89718cc..60be926 100644
--- a/src/AuthJanitor.Core/Providers/RekeyingAttemptLogger.cs
+++ b/src/AuthJanitor.Core/Providers/RekeyingAttemptLogger.cs
@@ -10,6 +10,7 @@ namespace AuthJanitor.Providers
public class RekeyingAttemptLogger : ILogger
{
public bool IsSuccessfulAttempt => string.IsNullOrEmpty(OuterException);
+ public bool IsComplete { get; set; }
public string OuterException { get; set; }
public DateTimeOffset AttemptStarted { get; set; }
diff --git a/src/AuthJanitor.Functions.AdminApi/AuthJanitor.Functions.AdminApi.csproj b/src/AuthJanitor.Functions.AdminApi/AuthJanitor.Functions.AdminApi.csproj
index 0350db6..40aeeb6 100644
--- a/src/AuthJanitor.Functions.AdminApi/AuthJanitor.Functions.AdminApi.csproj
+++ b/src/AuthJanitor.Functions.AdminApi/AuthJanitor.Functions.AdminApi.csproj
@@ -7,10 +7,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/AuthJanitor.Functions.AdminApi/Functions/AccessManagement.cs b/src/AuthJanitor.Functions.AdminApi/Functions/AccessManagement.cs
index 767d8eb..3e21b72 100644
--- a/src/AuthJanitor.Functions.AdminApi/Functions/AccessManagement.cs
+++ b/src/AuthJanitor.Functions.AdminApi/Functions/AccessManagement.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
+using Newtonsoft.Json;
using System.Threading.Tasks;
namespace AuthJanitor.Functions
@@ -24,10 +25,12 @@ namespace AuthJanitor.Functions
}
[FunctionName("AccessManagement-Add")]
- public async Task Add([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "access")] AuthJanitorAuthorizedUser newAuthorizedUserRole)
+ public async Task Add([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "access")] string userJson) //AuthJanitorAuthorizedUser newAuthorizedUserRole)
{
if (!_identityService.CurrentUserHasRole(AuthJanitorRoles.GlobalAdmin)) return new UnauthorizedResult();
+ var newAuthorizedUserRole = JsonConvert.DeserializeObject(userJson);
+
return await _managementService.AddAuthorizedUser(newAuthorizedUserRole.UPN, newAuthorizedUserRole.RoleValue);
}
diff --git a/src/AuthJanitor.Functions.AdminApi/Functions/ManagedSecrets.cs b/src/AuthJanitor.Functions.AdminApi/Functions/ManagedSecrets.cs
index 3bf1078..738124e 100644
--- a/src/AuthJanitor.Functions.AdminApi/Functions/ManagedSecrets.cs
+++ b/src/AuthJanitor.Functions.AdminApi/Functions/ManagedSecrets.cs
@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
+using Newtonsoft.Json;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -26,8 +27,10 @@ namespace AuthJanitor.Functions
}
[FunctionName("ManagedSecrets-Create")]
- public async Task Create([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets")] ManagedSecretViewModel inputSecret, CancellationToken cancellationToken)
+ public async Task Create([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets")] string secretJson, //ManagedSecretViewModel inputSecret,
+ CancellationToken cancellationToken)
{
+ var inputSecret = JsonConvert.DeserializeObject(secretJson);
return await _service.Create(inputSecret, cancellationToken);
}
@@ -53,9 +56,10 @@ namespace AuthJanitor.Functions
[FunctionName("ManagedSecrets-Update")]
public async Task Update(
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets/{secretId}")] ManagedSecretViewModel inputSecret,
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets/{secretId}")] string secretJson, //ManagedSecretViewModel inputSecret,
string secretId, CancellationToken cancellationToken)
{
+ var inputSecret = JsonConvert.DeserializeObject(secretJson);
return await _service.Update(inputSecret, Guid.Parse(secretId), cancellationToken);
}
}
diff --git a/src/AuthJanitor.Functions.AdminApi/Functions/Resources.cs b/src/AuthJanitor.Functions.AdminApi/Functions/Resources.cs
index 356029c..0b42681 100644
--- a/src/AuthJanitor.Functions.AdminApi/Functions/Resources.cs
+++ b/src/AuthJanitor.Functions.AdminApi/Functions/Resources.cs
@@ -1,67 +1,70 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT license.
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
using AuthJanitor.Services;
-using AuthJanitor.UI.Shared.ViewModels;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Azure.WebJobs;
-using Microsoft.Azure.WebJobs.Extensions.Http;
-using System;
+using AuthJanitor.UI.Shared.ViewModels;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.Http;
+using Newtonsoft.Json;
+using System;
using System.Threading;
-using System.Threading.Tasks;
+using System.Threading.Tasks;
-namespace AuthJanitor.Functions
-{
- ///
- /// API functions to control the creation and management of AuthJanitor Resources.
- /// A Resource is the description of how to connect to an object or resource, using a given Provider.
- ///
- public class Resources
- {
- private readonly ResourcesService _service;
-
- public Resources(ResourcesService service)
- {
- _service = service;
- }
-
- [FunctionName("Resources-Create")]
- public async Task Create(
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources")] ResourceViewModel resource,
- HttpRequest req, CancellationToken cancellationToken)
- {
- return await _service.Create(resource, req, cancellationToken);
- }
-
- [FunctionName("Resources-List")]
- public async Task List([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources")] HttpRequest req, CancellationToken cancellationToken)
- {
- return await _service.List(req, cancellationToken);
- }
-
- [FunctionName("Resources-Get")]
- public async Task Get(
- [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources/{resourceId}")] HttpRequest req,
- string resourceId, CancellationToken cancellationToken)
- {
- return await _service.Get(req, Guid.Parse(resourceId), cancellationToken);
- }
-
- [FunctionName("Resources-Delete")]
- public async Task Delete(
- [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "resources/{resourceId}")] HttpRequest req,
- string resourceId, CancellationToken cancellationToken)
- {
- return await _service.Delete(req, Guid.Parse(resourceId), cancellationToken);
- }
-
- [FunctionName("Resources-Update")]
- public async Task Update(
- [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources/{resourceId}")] ResourceViewModel resource,
- HttpRequest req,
- string resourceId, CancellationToken cancellationToken)
+namespace AuthJanitor.Functions
+{
+ ///
+ /// API functions to control the creation and management of AuthJanitor Resources.
+ /// A Resource is the description of how to connect to an object or resource, using a given Provider.
+ ///
+ public class Resources
+ {
+ private readonly ResourcesService _service;
+
+ public Resources(ResourcesService service)
{
- return await _service.Update(resource, req, Guid.Parse(resourceId), cancellationToken);
- }
- }
-}
+ _service = service;
+ }
+
+ [FunctionName("Resources-Create")]
+ public async Task Create(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources")] string resourceJson, /*ResourceViewModel resource,*/
+ HttpRequest req, CancellationToken cancellationToken)
+ {
+ var resource = JsonConvert.DeserializeObject(resourceJson);
+ return await _service.Create(resource, req, cancellationToken);
+ }
+
+ [FunctionName("Resources-List")]
+ public async Task List([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources")] HttpRequest req, CancellationToken cancellationToken)
+ {
+ return await _service.List(req, cancellationToken);
+ }
+
+ [FunctionName("Resources-Get")]
+ public async Task Get(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources/{resourceId}")] HttpRequest req,
+ string resourceId, CancellationToken cancellationToken)
+ {
+ return await _service.Get(req, Guid.Parse(resourceId), cancellationToken);
+ }
+
+ [FunctionName("Resources-Delete")]
+ public async Task Delete(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "resources/{resourceId}")] HttpRequest req,
+ string resourceId, CancellationToken cancellationToken)
+ {
+ return await _service.Delete(req, Guid.Parse(resourceId), cancellationToken);
+ }
+
+ [FunctionName("Resources-Update")]
+ public async Task Update(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources/{resourceId}")] string resourceJson, //ResourceViewModel resource,
+ HttpRequest req,
+ string resourceId, CancellationToken cancellationToken)
+ {
+ var resource = JsonConvert.DeserializeObject(resourceJson);
+ return await _service.Update(resource, req, Guid.Parse(resourceId), cancellationToken);
+ }
+ }
+}
diff --git a/src/AuthJanitor.Functions.AdminApi/Services/ProvidersService.cs b/src/AuthJanitor.Functions.AdminApi/Services/ProvidersService.cs
index a8aa710..ec32212 100644
--- a/src/AuthJanitor.Functions.AdminApi/Services/ProvidersService.cs
+++ b/src/AuthJanitor.Functions.AdminApi/Services/ProvidersService.cs
@@ -3,8 +3,8 @@
using AuthJanitor.UI.Shared;
using AuthJanitor.UI.Shared.MetaServices;
using AuthJanitor.UI.Shared.ViewModels;
-using AuthJanitor.EventSinks;
-using AuthJanitor.IdentityServices;
+using AuthJanitor.EventSinks;
+using AuthJanitor.IdentityServices;
using AuthJanitor.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -14,6 +14,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
+using AuthJanitor.Providers.Capabilities;
namespace AuthJanitor.Services
{
@@ -112,18 +113,23 @@ namespace AuthJanitor.Services
return new NotFoundResult();
}
- try
+ if (typeof(ICanRunSanityTests).IsAssignableFrom(provider.ProviderType))
{
- var instance = _providerManager.GetProviderInstance(provider.ProviderTypeName, providerConfiguration);
- if (instance == null)
- return new BadRequestErrorMessageResult("Provider configuration is invalid!");
- instance.Credential = credential;
- await instance.Test();
- }
- catch (Exception ex)
- {
- return new BadRequestErrorMessageResult(ex.Message);
+ try
+ {
+ var instance = _providerManager.GetProviderInstance(provider.ProviderTypeName, providerConfiguration);
+ if (instance == null)
+ return new BadRequestErrorMessageResult("Provider configuration is invalid!");
+ instance.Credential = credential;
+ await (instance as ICanRunSanityTests).Test();
+ }
+ catch (Exception ex)
+ {
+ return new BadRequestErrorMessageResult(ex.Message);
+ }
}
+ else
+ return new BadRequestErrorMessageResult("Provider does not support testing!");
return new OkResult();
}
diff --git a/src/AuthJanitor.Functions.AdminApi/host.json b/src/AuthJanitor.Functions.AdminApi/host.json
index 8857564..ba2a2e8 100644
--- a/src/AuthJanitor.Functions.AdminApi/host.json
+++ b/src/AuthJanitor.Functions.AdminApi/host.json
@@ -1,5 +1,6 @@
{
"version": "2.0",
+ "functionTimeout": "00:10:00",
"extensions": {
"http": {
"routePrefix": "api",
diff --git a/src/AuthJanitor.Functions.Agent/AuthJanitor.Functions.Agent.csproj b/src/AuthJanitor.Functions.Agent/AuthJanitor.Functions.Agent.csproj
index 37da20c..2722870 100644
--- a/src/AuthJanitor.Functions.Agent/AuthJanitor.Functions.Agent.csproj
+++ b/src/AuthJanitor.Functions.Agent/AuthJanitor.Functions.Agent.csproj
@@ -7,9 +7,12 @@
+
-
-
+
+
+
+
diff --git a/src/AuthJanitor.Integrations.CryptographicImplementations.Default/AuthJanitor.Integrations.CryptographicImplementations.Default.csproj b/src/AuthJanitor.Integrations.CryptographicImplementations.Default/AuthJanitor.Integrations.CryptographicImplementations.Default.csproj
index 0b9e8db..0945248 100644
--- a/src/AuthJanitor.Integrations.CryptographicImplementations.Default/AuthJanitor.Integrations.CryptographicImplementations.Default.csproj
+++ b/src/AuthJanitor.Integrations.CryptographicImplementations.Default/AuthJanitor.Integrations.CryptographicImplementations.Default.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Integrations.DataStores.AzureBlobStorage/AuthJanitor.Integrations.DataStores.AzureBlobStorage.csproj b/src/AuthJanitor.Integrations.DataStores.AzureBlobStorage/AuthJanitor.Integrations.DataStores.AzureBlobStorage.csproj
index 7de0cb1..a2a6d93 100644
--- a/src/AuthJanitor.Integrations.DataStores.AzureBlobStorage/AuthJanitor.Integrations.DataStores.AzureBlobStorage.csproj
+++ b/src/AuthJanitor.Integrations.DataStores.AzureBlobStorage/AuthJanitor.Integrations.DataStores.AzureBlobStorage.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Integrations.DataStores.EntityFrameworkCore/AuthJanitor.Integrations.DataStores.EntityFrameworkCore.csproj b/src/AuthJanitor.Integrations.DataStores.EntityFrameworkCore/AuthJanitor.Integrations.DataStores.EntityFrameworkCore.csproj
index 35855cc..b4ad320 100644
--- a/src/AuthJanitor.Integrations.DataStores.EntityFrameworkCore/AuthJanitor.Integrations.DataStores.EntityFrameworkCore.csproj
+++ b/src/AuthJanitor.Integrations.DataStores.EntityFrameworkCore/AuthJanitor.Integrations.DataStores.EntityFrameworkCore.csproj
@@ -5,8 +5,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/AuthJanitor.Integrations.EventSinks.SendGrid/AuthJanitor.Integrations.EventSinks.SendGrid.csproj b/src/AuthJanitor.Integrations.EventSinks.SendGrid/AuthJanitor.Integrations.EventSinks.SendGrid.csproj
index aa28a19..3ae53af 100644
--- a/src/AuthJanitor.Integrations.EventSinks.SendGrid/AuthJanitor.Integrations.EventSinks.SendGrid.csproj
+++ b/src/AuthJanitor.Integrations.EventSinks.SendGrid/AuthJanitor.Integrations.EventSinks.SendGrid.csproj
@@ -5,8 +5,8 @@
-
-
+
+
diff --git a/src/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory.csproj b/src/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory.csproj
index 4c17b9b..2c426cd 100644
--- a/src/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory.csproj
+++ b/src/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory/AuthJanitor.Integrations.IdentityServices.AzureActiveDirectory.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/AuthJanitor.Integrations.SecureStorage.AzureKeyVault/AuthJanitor.Integrations.SecureStorage.AzureKeyVault.csproj b/src/AuthJanitor.Integrations.SecureStorage.AzureKeyVault/AuthJanitor.Integrations.SecureStorage.AzureKeyVault.csproj
index 3189bfc..44f5ddb 100644
--- a/src/AuthJanitor.Integrations.SecureStorage.AzureKeyVault/AuthJanitor.Integrations.SecureStorage.AzureKeyVault.csproj
+++ b/src/AuthJanitor.Integrations.SecureStorage.AzureKeyVault/AuthJanitor.Integrations.SecureStorage.AzureKeyVault.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.AppServices/AuthJanitor.Providers.AppServices.csproj b/src/AuthJanitor.Providers.AppServices/AuthJanitor.Providers.AppServices.csproj
index eb91e93..40ad48d 100644
--- a/src/AuthJanitor.Providers.AppServices/AuthJanitor.Providers.AppServices.csproj
+++ b/src/AuthJanitor.Providers.AppServices/AuthJanitor.Providers.AppServices.csproj
@@ -6,8 +6,8 @@
-
-
+
+
diff --git a/src/AuthJanitor.Providers.AppServices/Functions/AppSettingsFunctionsApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.AppServices/Functions/AppSettingsFunctionsApplicationLifecycleProvider.cs
index dca3ff3..35af93c 100644
--- a/src/AuthJanitor.Providers.AppServices/Functions/AppSettingsFunctionsApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.AppServices/Functions/AppSettingsFunctionsApplicationLifecycleProvider.cs
@@ -16,9 +16,7 @@ namespace AuthJanitor.Providers.AppServices.Functions
///
[Provider(Name = "Functions App - AppSettings",
Description = "Manages the lifecycle of an Azure Functions app which reads a Managed Secret from its Application Settings",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.FUNCTIONS_SVG)]
+ SvgImage = ProviderImages.FUNCTIONS_SVG)]
public class AppSettingsFunctionsApplicationLifecycleProvider : SlottableAzureApplicationLifecycleProvider
{
private readonly ILogger _logger;
@@ -27,7 +25,7 @@ namespace AuthJanitor.Providers.AppServices.Functions
{
_logger = logger;
}
-
+
protected override async Task ApplyUpdate(IFunctionApp resource, string slotName, List secrets)
{
var updateBase = (await resource.DeploymentSlots.GetByNameAsync(slotName)).Update();
@@ -49,9 +47,14 @@ namespace AuthJanitor.Providers.AppServices.Functions
$"Functions application called {Configuration.ResourceName} (Resource Group " +
$"'{Configuration.ResourceGroup}'). During the rekeying, the Functions App will " +
$"be moved from slot '{Configuration.SourceSlot}' to slot '{Configuration.TemporarySlot}' " +
- $"temporarily, and then to slot '{Configuration.DestinationSlot}'.";
+ $"temporarily, and then back.";
- protected override Task SwapSlotAsync(IFunctionApp resource, string slotName) => resource.SwapAsync(slotName);
+ protected override async Task SwapSlotAsync(IFunctionApp resource, string sourceSlotName) =>
+ await resource.SwapAsync(sourceSlotName);
+
+ protected override async Task SwapSlotAsync(IFunctionApp resource, string sourceSlotName, string destinationSlotName) =>
+ await (await resource.DeploymentSlots.GetByNameAsync(destinationSlotName))
+ .SwapAsync(sourceSlotName);
protected override ISupportsGettingByResourceGroup GetResourceCollection(IAzure azure) => azure.AppServices.FunctionApps;
diff --git a/src/AuthJanitor.Providers.AppServices/Functions/ConnectionStringFunctionsApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.AppServices/Functions/ConnectionStringFunctionsApplicationLifecycleProvider.cs
index 496707c..270a884 100644
--- a/src/AuthJanitor.Providers.AppServices/Functions/ConnectionStringFunctionsApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.AppServices/Functions/ConnectionStringFunctionsApplicationLifecycleProvider.cs
@@ -13,9 +13,7 @@ namespace AuthJanitor.Providers.AppServices.Functions
{
[Provider(Name = "Functions App - Connection String",
Description = "Manages the lifecycle of an Azure Functions app which reads from a Connection String",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.FUNCTIONS_SVG)]
+ SvgImage = ProviderImages.FUNCTIONS_SVG)]
public class ConnectionStringFunctionsApplicationLifecycleProvider : SlottableAzureApplicationLifecycleProvider
{
private readonly ILogger _logger;
@@ -40,7 +38,12 @@ namespace AuthJanitor.Providers.AppServices.Functions
await updateBase.ApplyAsync();
}
- protected override Task SwapSlotAsync(IFunctionApp resource, string slotName) => resource.SwapAsync(slotName);
+ protected override async Task SwapSlotAsync(IFunctionApp resource, string sourceSlotName) =>
+ await resource.SwapAsync(sourceSlotName);
+
+ protected override async Task SwapSlotAsync(IFunctionApp resource, string sourceSlotName, string destinationSlotName) =>
+ await (await resource.DeploymentSlots.GetByNameAsync(destinationSlotName))
+ .SwapAsync(sourceSlotName);
protected override ISupportsGettingByResourceGroup GetResourceCollection(IAzure azure) => azure.AppServices.FunctionApps;
@@ -56,6 +59,6 @@ namespace AuthJanitor.Providers.AppServices.Functions
$"Functions application called {Configuration.ResourceName} (Resource Group " +
$"'{Configuration.ResourceGroup}'). During the rekeying, the Functions App will " +
$"be moved from slot '{Configuration.SourceSlot}' to slot '{Configuration.TemporarySlot}' " +
- $"temporarily, and then to slot '{Configuration.DestinationSlot}'.";
+ $"temporarily, and then back.";
}
}
diff --git a/src/AuthJanitor.Providers.AppServices/Functions/FunctionKeyRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.AppServices/Functions/FunctionKeyRekeyableObjectProvider.cs
index c39a835..bd79aa9 100644
--- a/src/AuthJanitor.Providers.AppServices/Functions/FunctionKeyRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.AppServices/Functions/FunctionKeyRekeyableObjectProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Azure.Management.AppService.Fluent;
using Microsoft.Azure.Management.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent.Core.CollectionActions;
@@ -13,9 +14,10 @@ namespace AuthJanitor.Providers.AppServices.Functions
{
[Provider(Name = "Functions App Key",
Description = "Regenerates a Function Key for an Azure Functions application",
- Features = ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.FUNCTIONS_SVG)]
- public class FunctionKeyRekeyableObjectProvider : AzureRekeyableObjectProvider
+ SvgImage = ProviderImages.FUNCTIONS_SVG)]
+ public class FunctionKeyRekeyableObjectProvider :
+ AzureRekeyableObjectProvider,
+ ICanRunSanityTests
{
private readonly ICryptographicImplementation _cryptographicImplementation;
private readonly ILogger _logger;
@@ -28,7 +30,7 @@ namespace AuthJanitor.Providers.AppServices.Functions
_cryptographicImplementation = cryptographicImplementation;
}
- public override async Task Test()
+ public async Task Test()
{
var functionsApp = await GetResourceAsync();
if (functionsApp == null)
@@ -66,10 +68,6 @@ namespace AuthJanitor.Providers.AppServices.Functions
$"Functions application called {Configuration.ResourceName} (Resource Group " +
$"'{Configuration.ResourceGroup}').";
- public override Task GetSecretToUseDuringRekeying() => Task.FromResult(null);
-
- public override Task OnConsumingApplicationSwapped() => Task.FromResult(0);
-
protected override ISupportsGettingByResourceGroup GetResourceCollection(IAzure azure) => azure.AppServices.FunctionApps;
// TODO: Zero-downtime rotation here with similar slotting?
diff --git a/src/AuthJanitor.Providers.AppServices/WebApps/AppSettingsWebAppApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.AppServices/WebApps/AppSettingsWebAppApplicationLifecycleProvider.cs
index 69cde85..d8f9e0e 100644
--- a/src/AuthJanitor.Providers.AppServices/WebApps/AppSettingsWebAppApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.AppServices/WebApps/AppSettingsWebAppApplicationLifecycleProvider.cs
@@ -12,10 +12,9 @@ namespace AuthJanitor.Providers.AppServices.WebApps
{
[Provider(Name = "WebApp - AppSettings",
Description = "Manages the lifecycle of an Azure Web App which reads a Managed Secret from its Application Settings",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.WEBAPPS_SVG)]
- public class AppSettingsWebAppApplicationLifecycleProvider : SlottableAzureApplicationLifecycleProvider
+ SvgImage = ProviderImages.WEBAPPS_SVG)]
+ public class AppSettingsWebAppApplicationLifecycleProvider :
+ SlottableAzureApplicationLifecycleProvider
{
private readonly ILogger _logger;
@@ -40,7 +39,12 @@ namespace AuthJanitor.Providers.AppServices.WebApps
await updateBase.ApplyAsync();
}
- protected override Task SwapSlotAsync(IWebApp resource, string slotName) => resource.SwapAsync(slotName);
+ protected override async Task SwapSlotAsync(IWebApp resource, string sourceSlotName) =>
+ await resource.SwapAsync(sourceSlotName);
+
+ protected override async Task SwapSlotAsync(IWebApp resource, string sourceSlotName, string destinationSlotName) =>
+ await (await resource.DeploymentSlots.GetByNameAsync(destinationSlotName))
+ .SwapAsync(sourceSlotName);
protected override ISupportsGettingByResourceGroup GetResourceCollection(IAzure azure) => azure.AppServices.WebApps;
@@ -55,6 +59,6 @@ namespace AuthJanitor.Providers.AppServices.WebApps
$"Web Application called {Configuration.ResourceName} (Resource Group " +
$"'{Configuration.ResourceGroup}'). During the rekeying, the Functions App will " +
$"be moved from slot '{Configuration.SourceSlot}' to slot '{Configuration.TemporarySlot}' " +
- $"temporarily, and then to slot '{Configuration.DestinationSlot}'.";
+ $"temporarily, and then back.";
}
}
diff --git a/src/AuthJanitor.Providers.AppServices/WebApps/ConnectionStringWebAppApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.AppServices/WebApps/ConnectionStringWebAppApplicationLifecycleProvider.cs
index 4c49141..3478f40 100644
--- a/src/AuthJanitor.Providers.AppServices/WebApps/ConnectionStringWebAppApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.AppServices/WebApps/ConnectionStringWebAppApplicationLifecycleProvider.cs
@@ -12,10 +12,9 @@ namespace AuthJanitor.Providers.AppServices.WebApps
{
[Provider(Name = "WebApp - Connection String",
Description = "Manages the lifecycle of an Azure Web App which reads from a Connection String",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.WEBAPPS_SVG)]
- public class ConnectionStringWebAppApplicationLifecycleProvider : SlottableAzureApplicationLifecycleProvider
+ SvgImage = ProviderImages.WEBAPPS_SVG)]
+ public class ConnectionStringWebAppApplicationLifecycleProvider :
+ SlottableAzureApplicationLifecycleProvider
{
private readonly ILogger _logger;
@@ -38,7 +37,12 @@ namespace AuthJanitor.Providers.AppServices.WebApps
await updateBase.ApplyAsync();
}
- protected override Task SwapSlotAsync(IWebApp resource, string slotName) => resource.SwapAsync(slotName);
+ protected override async Task SwapSlotAsync(IWebApp resource, string sourceSlotName) =>
+ await resource.SwapAsync(sourceSlotName);
+
+ protected override async Task SwapSlotAsync(IWebApp resource, string sourceSlotName, string destinationSlotName) =>
+ await (await resource.DeploymentSlots.GetByNameAsync(destinationSlotName))
+ .SwapAsync(sourceSlotName);
protected override ISupportsGettingByResourceGroup GetResourceCollection(IAzure azure) => azure.AppServices.WebApps;
@@ -54,6 +58,6 @@ namespace AuthJanitor.Providers.AppServices.WebApps
$"Web Application called {Configuration.ResourceName} (Resource Group " +
$"'{Configuration.ResourceGroup}'). During the rekeying, the Functions App will " +
$"be moved from slot '{Configuration.SourceSlot}' to slot '{Configuration.TemporarySlot}' " +
- $"temporarily, and then to slot '{Configuration.DestinationSlot}'.";
+ $"temporarily, and then back.";
}
}
diff --git a/src/AuthJanitor.Providers.Azure/AuthJanitor.Providers.Azure.csproj b/src/AuthJanitor.Providers.Azure/AuthJanitor.Providers.Azure.csproj
index 31e12ab..eda90f1 100644
--- a/src/AuthJanitor.Providers.Azure/AuthJanitor.Providers.Azure.csproj
+++ b/src/AuthJanitor.Providers.Azure/AuthJanitor.Providers.Azure.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.Azure/AzureApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.Azure/AzureApplicationLifecycleProvider.cs
index 68516d1..267a088 100644
--- a/src/AuthJanitor.Providers.Azure/AzureApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.Azure/AzureApplicationLifecycleProvider.cs
@@ -5,11 +5,10 @@ using System.Threading.Tasks;
namespace AuthJanitor.Providers.Azure
{
- public abstract class AzureApplicationLifecycleProvider : AzureAuthJanitorProvider, IApplicationLifecycleProvider
+ public abstract class AzureApplicationLifecycleProvider :
+ AzureAuthJanitorProvider, IApplicationLifecycleProvider
where TConfiguration : AzureAuthJanitorProviderConfiguration
{
- public abstract Task AfterRekeying();
- public abstract Task BeforeRekeying(List temporaryUseSecrets);
- public abstract Task CommitNewSecrets(List newSecrets);
+ public abstract Task DistributeLongTermSecretValues(List newSecretValues);
}
}
diff --git a/src/AuthJanitor.Providers.Azure/AzureAuthJanitorProviderConfiguration.cs b/src/AuthJanitor.Providers.Azure/AzureAuthJanitorProviderConfiguration.cs
index 501b40b..ad4466d 100644
--- a/src/AuthJanitor.Providers.Azure/AzureAuthJanitorProviderConfiguration.cs
+++ b/src/AuthJanitor.Providers.Azure/AzureAuthJanitorProviderConfiguration.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System;
using System.ComponentModel;
namespace AuthJanitor.Providers.Azure
@@ -15,5 +16,8 @@ namespace AuthJanitor.Providers.Azure
[DisplayName("Subscription ID")]
[Description("If this is left blank, the default subscription will be used.")]
public string SubscriptionId { get; set; }
+
+ public override int GenerateResourceIdentifierHashCode() =>
+ HashCode.Combine(SubscriptionId, ResourceGroup, ResourceName);
}
}
diff --git a/src/AuthJanitor.Providers.Azure/AzureRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.Azure/AzureRekeyableObjectProvider.cs
index 6494eb5..7802f08 100644
--- a/src/AuthJanitor.Providers.Azure/AzureRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.Azure/AzureRekeyableObjectProvider.cs
@@ -8,8 +8,6 @@ namespace AuthJanitor.Providers.Azure
public abstract class AzureRekeyableObjectProvider : AzureAuthJanitorProvider, IRekeyableObjectProvider
where TConfiguration : AzureAuthJanitorProviderConfiguration
{
- public abstract Task GetSecretToUseDuringRekeying();
- public abstract Task OnConsumingApplicationSwapped();
public abstract Task Rekey(TimeSpan requestedValidPeriod);
}
}
diff --git a/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureApplicationLifecycleProvider.cs
index d858cd8..8656666 100644
--- a/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureApplicationLifecycleProvider.cs
@@ -1,19 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace AuthJanitor.Providers.Azure.Workflows
{
- public abstract class SlottableAzureApplicationLifecycleProvider : AzureApplicationLifecycleProvider
+ public abstract class SlottableAzureApplicationLifecycleProvider :
+ AzureApplicationLifecycleProvider,
+ ICanRunSanityTests,
+ ICanDistributeTemporarySecretValues,
+ ICanPerformUnifiedCommitForTemporarySecretValues,
+ ICanPerformUnifiedCommit
where TConfiguration : SlottableAzureAuthJanitorProviderConfiguration
{
- private const string PRODUCTION_SLOT_NAME = "production";
+ protected const string PRODUCTION_SLOT_NAME = "production";
private readonly ILogger _logger;
protected SlottableAzureApplicationLifecycleProvider(ILogger logger) => _logger = logger;
- public override async Task Test()
+ public async Task Test()
{
var resource = await GetResourceAsync();
if (Configuration.SourceSlot != PRODUCTION_SLOT_NAME)
@@ -24,36 +30,41 @@ namespace AuthJanitor.Providers.Azure.Workflows
await TestSlotAsync(resource, Configuration.DestinationSlot);
}
- public override async Task AfterRekeying()
+ public async Task DistributeTemporarySecretValues(List secretValues)
{
- _logger.LogInformation("Swapping to '{SlotName}'", Configuration.TemporarySlot);
- await SwapSlotAsync(await GetResourceAsync(), Configuration.TemporarySlot);
- _logger.LogInformation("Swap complete!");
+ var resource = await GetResourceAsync();
+ await ApplyUpdate(resource, Configuration.TemporarySlot, secretValues);
}
- public override async Task BeforeRekeying(List temporaryUseSecrets)
+ public async Task UnifiedCommitForTemporarySecretValues()
{
- await ApplySecretSwap(temporaryUseSecrets);
- _logger.LogInformation("BeforeRekeying completed!");
+ _logger.LogInformation("Swapping settings from {TemporarySlot} to {SourceSlot}",
+ Configuration.TemporarySlot,
+ Configuration.SourceSlot);
+ var resource = await GetResourceAsync();
+ await SwapSlotAsync(resource, Configuration.TemporarySlot);
}
- public override async Task CommitNewSecrets(List newSecrets)
+ public override async Task DistributeLongTermSecretValues(List secretValues)
{
- await ApplySecretSwap(newSecrets);
- _logger.LogInformation("CommitNewSecrets completed!");
+ var resource = await GetResourceAsync();
+ await ApplyUpdate(resource, Configuration.TemporarySlot, secretValues);
+ }
+
+ public async Task UnifiedCommit()
+ {
+ _logger.LogInformation("Swapping from {SourceSlot} to {DestinationSlot}",
+ Configuration.TemporarySlot,
+ Configuration.DestinationSlot);
+ var resource = await GetResourceAsync();
+ await SwapSlotAsync(resource, Configuration.TemporarySlot);
}
protected abstract Task ApplyUpdate(TResource resource, string slotName, List secrets);
- protected abstract Task SwapSlotAsync(TResource resource, string slotName);
+ // applies to slot
+ protected abstract Task SwapSlotAsync(TResource resource, string sourceSlotName, string destinationSlotName);
+ // applies to prod
+ protected abstract Task SwapSlotAsync(TResource resource, string sourceSlotName);
protected abstract Task TestSlotAsync(TResource resource, string slotName);
-
- private async Task ApplySecretSwap(List secrets)
- {
- var resource = await GetResourceAsync();
- await ApplyUpdate(resource, Configuration.TemporarySlot, secrets);
-
- _logger.LogInformation("Swapping to '{SlotName}'", Configuration.TemporarySlot);
- await SwapSlotAsync(resource, Configuration.TemporarySlot);
- }
}
}
diff --git a/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureAuthJanitorProviderConfiguration.cs b/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureAuthJanitorProviderConfiguration.cs
index f48ff5c..7e088e3 100644
--- a/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureAuthJanitorProviderConfiguration.cs
+++ b/src/AuthJanitor.Providers.Azure/Workflows/SlottableAzureAuthJanitorProviderConfiguration.cs
@@ -8,7 +8,6 @@ namespace AuthJanitor.Providers.Azure.Workflows
{
public const string DEFAULT_ORIGINAL_SLOT = "production";
public const string DEFAULT_TEMPORARY_SLOT = "aj-temporary";
- public const string DEFAULT_DESTINATION_SLOT = DEFAULT_ORIGINAL_SLOT;
///
/// Source Slot (original application)
@@ -25,10 +24,10 @@ namespace AuthJanitor.Providers.Azure.Workflows
public string TemporarySlot { get; set; } = DEFAULT_TEMPORARY_SLOT;
///
- /// Destination Slot (updated application). By default this is the same as the Source Slot.
+ /// Destination Slot (where the app ends up)
///
[DisplayName("Destination Application Slot")]
- [Description("Slot to swap to by the end of the secret rotation process.")]
- public string DestinationSlot { get; set; } = DEFAULT_DESTINATION_SLOT;
+ [Description("Slot to copy the final settings to")]
+ public string DestinationSlot { get; set; } = DEFAULT_ORIGINAL_SLOT;
}
}
diff --git a/src/AuthJanitor.Providers.Azure/Workflows/TwoKeyAzureRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.Azure/Workflows/TwoKeyAzureRekeyableObjectProvider.cs
index 266bcfb..88d6c4b 100644
--- a/src/AuthJanitor.Providers.Azure/Workflows/TwoKeyAzureRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.Azure/Workflows/TwoKeyAzureRekeyableObjectProvider.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
@@ -9,7 +10,11 @@ using System.Threading.Tasks;
namespace AuthJanitor.Providers.Azure.Workflows
{
- public abstract class TwoKeyAzureRekeyableObjectProvider : AzureRekeyableObjectProvider
+ public abstract class TwoKeyAzureRekeyableObjectProvider :
+ AzureRekeyableObjectProvider,
+ ICanRunSanityTests,
+ ICanGenerateTemporarySecretValue,
+ ICanCleanup
where TConfiguration : TwoKeyAzureAuthJanitorProviderConfiguration
where TKeyType : struct, Enum
{
@@ -24,7 +29,7 @@ namespace AuthJanitor.Providers.Azure.Workflows
protected abstract RegeneratedSecret CreateSecretFromKeyring(TKeyring keyring, TKeyType keyType);
- public override async Task Test()
+ public async Task Test()
{
var resource = await GetResourceAsync();
if (resource == null) throw new Exception("Could not retrieve resource");
@@ -40,7 +45,7 @@ namespace AuthJanitor.Providers.Azure.Workflows
await Task.WhenAll(currentKeyringTask, otherKeyringTask);
}
- public override async Task GetSecretToUseDuringRekeying()
+ public async Task GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret to use during rekeying from other ({OtherKeyType}) key...", GetOtherPairedKey(Configuration.KeyType));
var resource = await GetResourceAsync();
@@ -67,7 +72,7 @@ namespace AuthJanitor.Providers.Azure.Workflows
return regeneratedSecret;
}
- public override async Task OnConsumingApplicationSwapped()
+ public async Task Cleanup()
{
if (!Configuration.SkipScramblingOtherKey)
{
diff --git a/src/AuthJanitor.Providers.AzureAD/AccessTokenConfiguration.cs b/src/AuthJanitor.Providers.AzureAD/AccessTokenConfiguration.cs
index 8fb0f58..3ad7608 100644
--- a/src/AuthJanitor.Providers.AzureAD/AccessTokenConfiguration.cs
+++ b/src/AuthJanitor.Providers.AzureAD/AccessTokenConfiguration.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System;
using System.ComponentModel;
namespace AuthJanitor.Providers.AzureAD
@@ -19,5 +20,8 @@ namespace AuthJanitor.Providers.AzureAD
[DisplayName("Auto-Refresh?")]
[Description("Allow AuthJanitor to automatically refresh the Access Token when it expires?")]
public bool AutomaticallyRefresh { get; set; }
+
+ public override int GenerateResourceIdentifierHashCode() =>
+ HashCode.Combine(Scopes);
}
}
diff --git a/src/AuthJanitor.Providers.AzureAD/AccessTokenRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.AzureAD/AccessTokenRekeyableObjectProvider.cs
index 2001557..40243a3 100644
--- a/src/AuthJanitor.Providers.AzureAD/AccessTokenRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.AzureAD/AccessTokenRekeyableObjectProvider.cs
@@ -12,9 +12,9 @@ namespace AuthJanitor.Providers.AzureAD
{
[Provider(Name = "Access Token",
Description = "Acquires an Access Token from Azure AD with a given set of scopes",
- Features = ProviderFeatureFlags.None)]
- [ProviderImage(ProviderImages.AZURE_AD_SVG)]
- public class AccessTokenRekeyableObjectProvider : RekeyableObjectProvider
+ SvgImage = ProviderImages.AZURE_AD_SVG)]
+ public class AccessTokenRekeyableObjectProvider :
+ RekeyableObjectProvider
{
private readonly ILogger _logger;
diff --git a/src/AuthJanitor.Providers.AzureAD/AuthJanitor.Providers.AzureAD.csproj b/src/AuthJanitor.Providers.AzureAD/AuthJanitor.Providers.AzureAD.csproj
index c922762..955f614 100644
--- a/src/AuthJanitor.Providers.AzureAD/AuthJanitor.Providers.AzureAD.csproj
+++ b/src/AuthJanitor.Providers.AzureAD/AuthJanitor.Providers.AzureAD.csproj
@@ -6,8 +6,8 @@
-
-
+
+
diff --git a/src/AuthJanitor.Providers.AzureMaps/AuthJanitor.Providers.AzureMaps.csproj b/src/AuthJanitor.Providers.AzureMaps/AuthJanitor.Providers.AzureMaps.csproj
index 9e1a79b..fe085c7 100644
--- a/src/AuthJanitor.Providers.AzureMaps/AuthJanitor.Providers.AzureMaps.csproj
+++ b/src/AuthJanitor.Providers.AzureMaps/AuthJanitor.Providers.AzureMaps.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.AzureMaps/AzureMapsRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.AzureMaps/AzureMapsRekeyableObjectProvider.cs
index ced91e8..0a64c74 100644
--- a/src/AuthJanitor.Providers.AzureMaps/AzureMapsRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.AzureMaps/AzureMapsRekeyableObjectProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Azure.Management.Maps;
using Microsoft.Azure.Management.Maps.Models;
using Microsoft.Extensions.Logging;
@@ -13,11 +14,11 @@ namespace AuthJanitor.Providers.AzureMaps
{
[Provider(Name = "Azure Maps Key",
Description = "Regenerates a key for an Azure Maps instance",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.AZURE_MAPS_SVG)]
- public class AzureMapsRekeyableObjectProvider : RekeyableObjectProvider
+ SvgImage = ProviderImages.AZURE_MAPS_SVG)]
+ public class AzureMapsRekeyableObjectProvider :
+ RekeyableObjectProvider,
+ ICanRunSanityTests,
+ ICanGenerateTemporarySecretValue
{
private const string PRIMARY_KEY = "primary";
private const string SECONDARY_KEY = "secondary";
@@ -29,7 +30,7 @@ namespace AuthJanitor.Providers.AzureMaps
_logger = logger;
}
- public override async Task Test()
+ public async Task Test()
{
var keys = await ManagementClient.Accounts.ListKeysAsync(
Configuration.ResourceGroup,
@@ -37,7 +38,7 @@ namespace AuthJanitor.Providers.AzureMaps
if (keys == null) throw new Exception("Could not access Azure Maps keys");
}
- public override async Task GetSecretToUseDuringRekeying()
+ public async Task GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret to use during rekeying from other ({OtherKeyType}) key...", GetOtherKeyType);
var keys = await ManagementClient.Accounts.ListKeysAsync(
@@ -68,7 +69,7 @@ namespace AuthJanitor.Providers.AzureMaps
};
}
- public override async Task OnConsumingApplicationSwapped()
+ public async Task Cleanup()
{
if (!Configuration.SkipScramblingOtherKey)
{
diff --git a/src/AuthJanitor.Providers.AzureSearch/AzureSearchAdminKeyRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.AzureSearch/AzureSearchAdminKeyRekeyableObjectProvider.cs
index bbf4e6b..0d33c4f 100644
--- a/src/AuthJanitor.Providers.AzureSearch/AzureSearchAdminKeyRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.AzureSearch/AzureSearchAdminKeyRekeyableObjectProvider.cs
@@ -14,10 +14,7 @@ namespace AuthJanitor.Providers.AzureSearch
{
[Provider(Name = "Azure Search Admin Key",
Description = "Regenerates an Admin Key for an Azure Search service",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.AZURE_SEARCH_SVG)]
+ SvgImage = ProviderImages.AZURE_SEARCH_SVG)]
public class AzureSearchAdminKeyRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider
{
public AzureSearchAdminKeyRekeyableObjectProvider(ILogger logger) : base(logger) { }
diff --git a/src/AuthJanitor.Providers.AzureSql/AuthJanitor.Providers.AzureSql.csproj b/src/AuthJanitor.Providers.AzureSql/AuthJanitor.Providers.AzureSql.csproj
index e149a60..df78108 100644
--- a/src/AuthJanitor.Providers.AzureSql/AuthJanitor.Providers.AzureSql.csproj
+++ b/src/AuthJanitor.Providers.AzureSql/AuthJanitor.Providers.AzureSql.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.AzureSql/AzureSqlAdministratorPasswordRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.AzureSql/AzureSqlAdministratorPasswordRekeyableObjectProvider.cs
index 12f3abf..2b3f8ce 100644
--- a/src/AuthJanitor.Providers.AzureSql/AzureSqlAdministratorPasswordRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.AzureSql/AzureSqlAdministratorPasswordRekeyableObjectProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Microsoft.Azure.Management.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent.Core.CollectionActions;
using Microsoft.Azure.Management.Sql.Fluent;
@@ -15,9 +16,10 @@ namespace AuthJanitor.Providers.AzureSql
{
[Provider(Name = "Azure SQL Server Administrator Password",
Description = "Regenerates the administrator password of an Azure SQL Server",
- Features = ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.SQL_SERVER_SVG)]
- public class AzureSqlAdministratorPasswordRekeyableObjectProvider : AzureRekeyableObjectProvider
+ SvgImage = ProviderImages.SQL_SERVER_SVG)]
+ public class AzureSqlAdministratorPasswordRekeyableObjectProvider :
+ AzureRekeyableObjectProvider,
+ ICanRunSanityTests
{
private readonly ICryptographicImplementation _cryptographicImplementation;
private readonly ILogger _logger;
@@ -30,15 +32,13 @@ namespace AuthJanitor.Providers.AzureSql
_cryptographicImplementation = cryptographicImplementation;
}
- public override async Task Test()
+ public async Task Test()
{
var sqlServer = await GetResourceAsync();
if (sqlServer == null)
throw new Exception($"Cannot locate Azure Sql server called '{Configuration.ResourceName}' in group '{Configuration.ResourceGroup}'");
}
-
- public override Task GetSecretToUseDuringRekeying() => Task.FromResult(null);
-
+
public override async Task Rekey(TimeSpan requestedValidPeriod)
{
_logger.LogInformation("Generating new password of length {PasswordLength}", Configuration.PasswordLength);
@@ -60,8 +60,6 @@ namespace AuthJanitor.Providers.AzureSql
};
}
- public override Task OnConsumingApplicationSwapped() => Task.FromResult(0);
-
public override IList GetRisks()
{
List issues = new List();
diff --git a/src/AuthJanitor.Providers.CosmosDb/AuthJanitor.Providers.CosmosDb.csproj b/src/AuthJanitor.Providers.CosmosDb/AuthJanitor.Providers.CosmosDb.csproj
index 521b8fe..e4205b0 100644
--- a/src/AuthJanitor.Providers.CosmosDb/AuthJanitor.Providers.CosmosDb.csproj
+++ b/src/AuthJanitor.Providers.CosmosDb/AuthJanitor.Providers.CosmosDb.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.CosmosDb/CosmosDbRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.CosmosDb/CosmosDbRekeyableObjectProvider.cs
index b48231c..770f5d3 100644
--- a/src/AuthJanitor.Providers.CosmosDb/CosmosDbRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.CosmosDb/CosmosDbRekeyableObjectProvider.cs
@@ -13,10 +13,7 @@ namespace AuthJanitor.Providers.CosmosDb
{
[Provider(Name = "CosmosDB Master Key",
Description = "Regenerates a Master Key for an Azure CosmosDB instance",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.COSMOS_DB_SVG)]
+ SvgImage = ProviderImages.COSMOS_DB_SVG)]
public class CosmosDbRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider
{
private const string PRIMARY_READONLY_KEY = "primaryReadOnly";
diff --git a/src/AuthJanitor.Providers.EventHub/AuthJanitor.Providers.EventHub.csproj b/src/AuthJanitor.Providers.EventHub/AuthJanitor.Providers.EventHub.csproj
index 9f5bf7e..d14dd96 100644
--- a/src/AuthJanitor.Providers.EventHub/AuthJanitor.Providers.EventHub.csproj
+++ b/src/AuthJanitor.Providers.EventHub/AuthJanitor.Providers.EventHub.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.EventHub/EventHubRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.EventHub/EventHubRekeyableObjectProvider.cs
index 0d34ded..74bc2f5 100644
--- a/src/AuthJanitor.Providers.EventHub/EventHubRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.EventHub/EventHubRekeyableObjectProvider.cs
@@ -15,10 +15,7 @@ namespace AuthJanitor.Providers.EventHub
{
[Provider(Name = "Event Hub Key",
Description = "Regenerates an Azure Event Hub Key",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.EVENT_HUB_SVG)]
+ SvgImage = ProviderImages.EVENT_HUB_SVG)]
public class EventHubRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider
{
public EventHubRekeyableObjectProvider(ILogger logger) : base(logger) { }
diff --git a/src/AuthJanitor.Providers.KeyVault/AuthJanitor.Providers.KeyVault.csproj b/src/AuthJanitor.Providers.KeyVault/AuthJanitor.Providers.KeyVault.csproj
index 0b478d5..36bc05d 100644
--- a/src/AuthJanitor.Providers.KeyVault/AuthJanitor.Providers.KeyVault.csproj
+++ b/src/AuthJanitor.Providers.KeyVault/AuthJanitor.Providers.KeyVault.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyConfiguration.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyConfiguration.cs
index 2a0a0f5..fe2a122 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyConfiguration.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyConfiguration.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System;
using System.ComponentModel;
namespace AuthJanitor.Providers.KeyVault
@@ -19,5 +20,8 @@ namespace AuthJanitor.Providers.KeyVault
[DisplayName("Key Name")]
[Description("Key Name to manage")]
public string KeyName { get; set; }
+
+ public override int GenerateResourceIdentifierHashCode() =>
+ HashCode.Combine(VaultName);
}
}
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyRekeyableObjectProvider.cs
index aff915e..ad24f32 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultKeyRekeyableObjectProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Azure;
using Azure.Security.KeyVault.Keys;
using Microsoft.Extensions.Logging;
@@ -14,11 +15,11 @@ namespace AuthJanitor.Providers.KeyVault
{
[Provider(Name = "Key Vault Key",
Description = "Regenerates an Azure Key Vault Key with the same parameters as the previous version",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.KEY_VAULT_SVG)]
- public class KeyVaultKeyRekeyableObjectProvider : RekeyableObjectProvider
+ SvgImage = ProviderImages.KEY_VAULT_SVG)]
+ public class KeyVaultKeyRekeyableObjectProvider :
+ RekeyableObjectProvider,
+ ICanRunSanityTests,
+ ICanGenerateTemporarySecretValue
{
private readonly ILogger _logger;
@@ -27,13 +28,13 @@ namespace AuthJanitor.Providers.KeyVault
_logger = logger;
}
- public override async Task Test()
+ public async Task Test()
{
var key = await GetKeyClient().GetKeyAsync(Configuration.KeyName);
if (key == null) throw new Exception("Key was not found or not accessible");
}
- public override async Task GetSecretToUseDuringRekeying()
+ public async Task GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret to use during rekeying based on current KID");
var client = GetKeyClient();
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretApplicationLifecycleProvider.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretApplicationLifecycleProvider.cs
index 963d6ce..0b72263 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretApplicationLifecycleProvider.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretApplicationLifecycleProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Azure;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Logging;
@@ -13,9 +14,10 @@ namespace AuthJanitor.Providers.KeyVault
{
[Provider(Name = "Key Vault Secret",
Description = "Manages the lifecycle of a Key Vault Secret where a Managed Secret's value is stored",
- Features = ProviderFeatureFlags.IsTestable)]
- [ProviderImage(ProviderImages.KEY_VAULT_SVG)]
- public class KeyVaultSecretApplicationLifecycleProvider : ApplicationLifecycleProvider
+ SvgImage = ProviderImages.KEY_VAULT_SVG)]
+ public class KeyVaultSecretApplicationLifecycleProvider :
+ ApplicationLifecycleProvider,
+ ICanRunSanityTests
{
private readonly ILogger _logger;
@@ -24,20 +26,17 @@ namespace AuthJanitor.Providers.KeyVault
_logger = logger;
}
- public override async Task Test()
+ public async Task Test()
{
var secret = await GetSecretClient().GetSecretAsync(Configuration.SecretName);
if (secret == null) throw new Exception("Could not access Key Vault Secret");
}
-
- ///
- /// Call to commit the newly generated secret
- ///
- public override async Task CommitNewSecrets(List newSecrets)
+
+ public override async Task DistributeLongTermSecretValues(List newSecretValues)
{
_logger.LogInformation("Committing new secrets to Key Vault secret {SecretName}", Configuration.SecretName);
var client = GetSecretClient();
- foreach (RegeneratedSecret secret in newSecrets)
+ foreach (RegeneratedSecret secret in newSecretValues)
{
_logger.LogInformation("Getting current secret version from secret name {SecretName}", Configuration.SecretName);
Response currentSecret = await client.GetSecretAsync(Configuration.SecretName);
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretConfiguration.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretConfiguration.cs
index 508dd4d..7e21c84 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretConfiguration.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretConfiguration.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System;
using System.ComponentModel;
namespace AuthJanitor.Providers.KeyVault
@@ -28,5 +29,8 @@ namespace AuthJanitor.Providers.KeyVault
[DisplayName("Secret Length")]
[Description("Length of secret to generate")]
public int SecretLength { get; set; } = DEFAULT_SECRET_LENGTH;
+
+ public override int GenerateResourceIdentifierHashCode() =>
+ HashCode.Combine(VaultName);
}
}
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretLifecycleConfiguration.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretLifecycleConfiguration.cs
index 9b106ac..2e7f5d7 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretLifecycleConfiguration.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretLifecycleConfiguration.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
+using System;
using System.ComponentModel;
namespace AuthJanitor.Providers.KeyVault
@@ -26,5 +27,8 @@ namespace AuthJanitor.Providers.KeyVault
[DisplayName("Commit Connection String")]
[Description("Commit a Connection String instead of a Key to this AppSetting, when available")]
public bool CommitAsConnectionString { get; set; }
+
+ public override int GenerateResourceIdentifierHashCode() =>
+ HashCode.Combine(VaultName);
}
}
diff --git a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretRekeyableObjectProvider.cs
index c6d1cf8..a56301b 100644
--- a/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.KeyVault/KeyVaultSecretRekeyableObjectProvider.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using AuthJanitor.Integrations.CryptographicImplementations;
using AuthJanitor.Providers.Azure;
+using AuthJanitor.Providers.Capabilities;
using Azure;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Logging;
@@ -14,11 +15,11 @@ namespace AuthJanitor.Providers.KeyVault
{
[Provider(Name = "Key Vault Secret",
Description = "Regenerates a Key Vault Secret with a given length",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.KEY_VAULT_SVG)]
- public class KeyVaultSecretRekeyableObjectProvider : RekeyableObjectProvider
+ SvgImage = ProviderImages.KEY_VAULT_SVG)]
+ public class KeyVaultSecretRekeyableObjectProvider :
+ RekeyableObjectProvider,
+ ICanRunSanityTests,
+ ICanGenerateTemporarySecretValue
{
private readonly ICryptographicImplementation _cryptographicImplementation;
private readonly ILogger _logger;
@@ -31,13 +32,13 @@ namespace AuthJanitor.Providers.KeyVault
_cryptographicImplementation = cryptographicImplementation;
}
- public override async Task Test()
+ public async Task Test()
{
var secret = await GetSecretClient().GetSecretAsync(Configuration.SecretName);
if (secret == null) throw new Exception("Could not access Key Vault Secret");
}
- public override async Task GetSecretToUseDuringRekeying()
+ public async Task GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret based on current version...");
var client = GetSecretClient();
diff --git a/src/AuthJanitor.Providers.RedisCache/AuthJanitor.Providers.RedisCache.csproj b/src/AuthJanitor.Providers.RedisCache/AuthJanitor.Providers.RedisCache.csproj
index b71c3a9..6cdf16b 100644
--- a/src/AuthJanitor.Providers.RedisCache/AuthJanitor.Providers.RedisCache.csproj
+++ b/src/AuthJanitor.Providers.RedisCache/AuthJanitor.Providers.RedisCache.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.RedisCache/RedisCacheKeyRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.RedisCache/RedisCacheKeyRekeyableObjectProvider.cs
index 3341ece..1881c5c 100644
--- a/src/AuthJanitor.Providers.RedisCache/RedisCacheKeyRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.RedisCache/RedisCacheKeyRekeyableObjectProvider.cs
@@ -14,10 +14,7 @@ namespace AuthJanitor.Providers.Redis
{
[Provider(Name = "Redis Cache Key",
Description = "Regenerates a Master Key for a Redis Cache instance",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.REDIS_SVG)]
+ SvgImage = ProviderImages.REDIS_SVG)]
public class RedisCacheKeyRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider
{
protected override string Service => "Redis Cache";
diff --git a/src/AuthJanitor.Providers.ServiceBus/AuthJanitor.Providers.ServiceBus.csproj b/src/AuthJanitor.Providers.ServiceBus/AuthJanitor.Providers.ServiceBus.csproj
index 1c4cd55..047b802 100644
--- a/src/AuthJanitor.Providers.ServiceBus/AuthJanitor.Providers.ServiceBus.csproj
+++ b/src/AuthJanitor.Providers.ServiceBus/AuthJanitor.Providers.ServiceBus.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.ServiceBus/ServiceBusRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.ServiceBus/ServiceBusRekeyableObjectProvider.cs
index eb79bf3..d0eef64 100644
--- a/src/AuthJanitor.Providers.ServiceBus/ServiceBusRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.ServiceBus/ServiceBusRekeyableObjectProvider.cs
@@ -14,10 +14,7 @@ namespace AuthJanitor.Providers.ServiceBus
{
[Provider(Name = "Service Bus Key",
Description = "Regenerates an Azure Service Bus Key",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.SERVICE_BUS_SVG)]
+ SvgImage = ProviderImages.SERVICE_BUS_SVG)]
public class ServiceBusRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider
{
public ServiceBusRekeyableObjectProvider(ILogger logger) : base(logger) { }
diff --git a/src/AuthJanitor.Providers.Storage/AuthJanitor.Providers.Storage.csproj b/src/AuthJanitor.Providers.Storage/AuthJanitor.Providers.Storage.csproj
index 1c4cd55..047b802 100644
--- a/src/AuthJanitor.Providers.Storage/AuthJanitor.Providers.Storage.csproj
+++ b/src/AuthJanitor.Providers.Storage/AuthJanitor.Providers.Storage.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/src/AuthJanitor.Providers.Storage/StorageAccountRekeyableObjectProvider.cs b/src/AuthJanitor.Providers.Storage/StorageAccountRekeyableObjectProvider.cs
index 5c190f1..80d242c 100644
--- a/src/AuthJanitor.Providers.Storage/StorageAccountRekeyableObjectProvider.cs
+++ b/src/AuthJanitor.Providers.Storage/StorageAccountRekeyableObjectProvider.cs
@@ -16,10 +16,7 @@ namespace AuthJanitor.Providers.Storage
{
[Provider(Name = "Storage Account Key",
Description = "Regenerates a key of a specified type for an Azure Storage Account",
- Features = ProviderFeatureFlags.CanRotateWithoutDowntime |
- ProviderFeatureFlags.IsTestable |
- ProviderFeatureFlags.SupportsSecondaryKey)]
- [ProviderImage(ProviderImages.STORAGE_ACCOUNT_SVG)]
+ SvgImage = ProviderImages.STORAGE_ACCOUNT_SVG)]
public class StorageAccountRekeyableObjectProvider : TwoKeyAzureRekeyableObjectProvider, StorageAccountKeyConfiguration.StorageKeyTypes, string>
{
private const string KEY1 = "key1";
@@ -45,7 +42,20 @@ namespace AuthJanitor.Providers.Storage
protected override Task> RetrieveCurrentKeyring(IStorageAccount resource, string keyType) => resource.GetKeysAsync();
- protected override Task> RotateKeyringValue(IStorageAccount resource, string keyType) => resource.RegenerateKeyAsync(keyType);
+ protected override async Task> RotateKeyringValue(IStorageAccount resource, string keyType)
+ {
+ var result = await resource.RegenerateKeyAsync(keyType);
+ var expectedNewKey = result.FirstOrDefault(k => k.KeyName == keyType);
+ var end = DateTime.Now.AddMinutes(2);
+ while (DateTime.Now < end)
+ {
+ var keyring = await RetrieveCurrentKeyring(resource, keyType);
+ var key = keyring.FirstOrDefault(k => k.KeyName == keyType);
+ if (key == null) continue;
+ if (key.Value == expectedNewKey.Value) return result;
+ }
+ throw new Exception("Storage key was reported as rotated, but didn't resync within 2 minutes!");
+ }
protected override string Translate(StorageAccountKeyConfiguration.StorageKeyTypes keyType) => keyType switch
{
diff --git a/src/AuthJanitor.Tests/AuthJanitor.Tests.csproj b/src/AuthJanitor.Tests/AuthJanitor.Tests.csproj
index 260f4b7..baddf34 100644
--- a/src/AuthJanitor.Tests/AuthJanitor.Tests.csproj
+++ b/src/AuthJanitor.Tests/AuthJanitor.Tests.csproj
@@ -7,11 +7,17 @@
-
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+