From 8ed075aeacbf9b0f0c5c5f373d0466a98b06639d Mon Sep 17 00:00:00 2001 From: Anthony Turner <225599+anthturner@users.noreply.github.com> Date: Thu, 16 Jul 2020 01:38:57 -0700 Subject: [PATCH] Fix provider collisions, UI improvements (#95) * Fix provider collisions, UI improvements * UI fixes, bolstering Storage --- .../AuthJanitor.AspNet.AdminUi.csproj | 8 +- .../ProviderCapabilitiesComponent.razor | 48 ++ .../Components/TaskProgressionComponent.razor | 89 ++++ .../Editors/ManagedSecretEditor.razor | 2 +- .../Pages/ManagedSecretDetail.razor | 28 +- .../Pages/ManagedSecrets.razor | 2 +- .../Pages/Providers.razor | 23 +- .../Pages/RekeyingTaskDetail.razor | 284 ++++++------ .../Pages/RekeyingTasks.razor | 11 +- .../Pages/ResourceDetail.razor | 20 +- .../wwwroot/css/AJAnimated.svg | 2 + .../wwwroot/index.html | 36 +- .../AuthJanitor.AspNet.Core.csproj | 2 +- .../MetaServices/TaskExecutionMetaService.cs | 418 +++++++++--------- src/AuthJanitor.AspNet/ViewModelFactory.cs | 25 +- .../ViewModels/LoadedProviderViewModel.cs | 16 +- .../ProviderConfigurationViewModel.cs | 1 - src/AuthJanitor.Core/AuthJanitor.Core.csproj | 4 +- .../Providers/ApplicationLifecycleProvider.cs | 32 +- .../Providers/AuthJanitorProvider.cs | 21 +- .../AuthJanitorProviderConfiguration.cs | 7 + .../Capabilities/IAuthJanitorCapability.cs | 7 + .../Providers/Capabilities/ICanCleanup.cs | 11 + .../ICanDistributeTemporarySecretValues.cs | 12 + .../ICanEnumerateResourceCandidates.cs | 13 + .../ICanGenerateTemporarySecretValue.cs | 12 + .../Capabilities/ICanPerformUnifiedCommit.cs | 11 + ...rmUnifiedCommitForTemporarySecretValues.cs | 11 + .../Capabilities/ICanRunSanityTests.cs | 11 + .../Providers/LoadedProviderMetadata.cs | 5 - .../Providers/ProviderAttribute.cs | 23 - .../Providers/ProviderManagerService.cs | 201 ++++++--- .../Providers/RekeyableObjectProvider.cs | 32 -- .../Providers/RekeyingAttemptLogger.cs | 1 + .../AuthJanitor.Functions.AdminApi.csproj | 8 +- .../Functions/AccessManagement.cs | 5 +- .../Functions/ManagedSecrets.cs | 8 +- .../Functions/Resources.cs | 129 +++--- .../Services/ProvidersService.cs | 30 +- src/AuthJanitor.Functions.AdminApi/host.json | 1 + .../AuthJanitor.Functions.Agent.csproj | 7 +- ...ryptographicImplementations.Default.csproj | 2 +- ...rations.DataStores.AzureBlobStorage.csproj | 2 +- ...ions.DataStores.EntityFrameworkCore.csproj | 4 +- ...or.Integrations.EventSinks.SendGrid.csproj | 4 +- ...entityServices.AzureActiveDirectory.csproj | 2 +- ...rations.SecureStorage.AzureKeyVault.csproj | 2 +- .../AuthJanitor.Providers.AppServices.csproj | 4 +- ...gsFunctionsApplicationLifecycleProvider.cs | 15 +- ...ngFunctionsApplicationLifecycleProvider.cs | 13 +- .../FunctionKeyRekeyableObjectProvider.cs | 14 +- ...tingsWebAppApplicationLifecycleProvider.cs | 16 +- ...tringWebAppApplicationLifecycleProvider.cs | 16 +- .../AuthJanitor.Providers.Azure.csproj | 2 +- .../AzureApplicationLifecycleProvider.cs | 7 +- .../AzureAuthJanitorProviderConfiguration.cs | 4 + .../AzureRekeyableObjectProvider.cs | 2 - ...ttableAzureApplicationLifecycleProvider.cs | 57 ++- ...leAzureAuthJanitorProviderConfiguration.cs | 7 +- .../TwoKeyAzureRekeyableObjectProvider.cs | 13 +- .../AccessTokenConfiguration.cs | 4 + .../AccessTokenRekeyableObjectProvider.cs | 6 +- .../AuthJanitor.Providers.AzureAD.csproj | 4 +- .../AuthJanitor.Providers.AzureMaps.csproj | 2 +- .../AzureMapsRekeyableObjectProvider.cs | 17 +- ...reSearchAdminKeyRekeyableObjectProvider.cs | 5 +- .../AuthJanitor.Providers.AzureSql.csproj | 2 +- ...istratorPasswordRekeyableObjectProvider.cs | 16 +- .../AuthJanitor.Providers.CosmosDb.csproj | 2 +- .../CosmosDbRekeyableObjectProvider.cs | 5 +- .../AuthJanitor.Providers.EventHub.csproj | 2 +- .../EventHubRekeyableObjectProvider.cs | 5 +- .../AuthJanitor.Providers.KeyVault.csproj | 2 +- .../KeyVaultKeyConfiguration.cs | 4 + .../KeyVaultKeyRekeyableObjectProvider.cs | 15 +- ...VaultSecretApplicationLifecycleProvider.cs | 19 +- .../KeyVaultSecretConfiguration.cs | 4 + .../KeyVaultSecretLifecycleConfiguration.cs | 4 + .../KeyVaultSecretRekeyableObjectProvider.cs | 15 +- .../AuthJanitor.Providers.RedisCache.csproj | 2 +- .../RedisCacheKeyRekeyableObjectProvider.cs | 5 +- .../AuthJanitor.Providers.ServiceBus.csproj | 2 +- .../ServiceBusRekeyableObjectProvider.cs | 5 +- .../AuthJanitor.Providers.Storage.csproj | 2 +- .../StorageAccountRekeyableObjectProvider.cs | 20 +- .../AuthJanitor.Tests.csproj | 16 +- 86 files changed, 1176 insertions(+), 812 deletions(-) create mode 100644 src/AuthJanitor.AspNet.AdminUi/Components/ProviderCapabilitiesComponent.razor create mode 100644 src/AuthJanitor.AspNet.AdminUi/Components/TaskProgressionComponent.razor create mode 100644 src/AuthJanitor.AspNet.AdminUi/wwwroot/css/AJAnimated.svg create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/IAuthJanitorCapability.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanCleanup.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanDistributeTemporarySecretValues.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanEnumerateResourceCandidates.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanGenerateTemporarySecretValue.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommit.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanPerformUnifiedCommitForTemporarySecretValues.cs create mode 100644 src/AuthJanitor.Core/Providers/Capabilities/ICanRunSanityTests.cs diff --git a/src/AuthJanitor.AspNet.AdminUi/AuthJanitor.AspNet.AdminUi.csproj b/src/AuthJanitor.AspNet.AdminUi/AuthJanitor.AspNet.AdminUi.csproj index 2b96312..6c97de8 100644 --- a/src/AuthJanitor.AspNet.AdminUi/AuthJanitor.AspNet.AdminUi.csproj +++ b/src/AuthJanitor.AspNet.AdminUi/AuthJanitor.AspNet.AdminUi.csproj @@ -18,11 +18,11 @@ - - - + + + - + diff --git a/src/AuthJanitor.AspNet.AdminUi/Components/ProviderCapabilitiesComponent.razor b/src/AuthJanitor.AspNet.AdminUi/Components/ProviderCapabilitiesComponent.razor new file mode 100644 index 0000000..a474376 --- /dev/null +++ b/src/AuthJanitor.AspNet.AdminUi/Components/ProviderCapabilitiesComponent.razor @@ -0,0 +1,48 @@ +@{ + void RenderCapabilityIcon(string icon, string templateString, bool isLit) + { + + } + + RenderCapabilityIcon( + icon: FontAwesomeIcons.Flask, + templateString: "Provider {0} running sanity tests", + isLit: HasCapabilities(ProviderCapabilities.CanRunSanityTests)); + + RenderCapabilityIcon( + icon: FontAwesomeIcons.SyncAlt, + templateString: "Provider {0} generating/consuming interim secret values", + isLit: HasCapabilitiesOr(ProviderCapabilities.CanDistributeTemporarySecrets, + ProviderCapabilities.CanGenerateTemporarySecrets)); + + RenderCapabilityIcon( + icon: FontAwesomeIcons.LayerGroup, + templateString: "Provider {0} resource candidate selection for configuration", + isLit: HasCapabilities(ProviderCapabilities.CanEnumerateResourceCandidates)); +} + +@code { + [Parameter] + public LoadedProviderViewModel Provider { get; set; } + + [Parameter] + public string IconLitColor { get; set; } = "text-success"; + + [Parameter] + public string IconUnlitColor { get; set; } = "text-dark"; + + private const string SUPPORTS = "supports"; + private const string DOES_NOT_SUPPORT = "does not support"; + + private static string GetTitle(string templateString, bool isLit) => + string.Format(templateString, isLit ? SUPPORTS : DOES_NOT_SUPPORT); + + private bool HasCapabilities(params ProviderCapabilities[] capabilities) => + !capabilities.Except(Provider.Capabilities).Any(); + + private bool HasCapabilitiesOr(params ProviderCapabilities[] capabilities) => + Provider.Capabilities.Any(c => capabilities.Contains(c)); +} \ No newline at end of file diff --git a/src/AuthJanitor.AspNet.AdminUi/Components/TaskProgressionComponent.razor b/src/AuthJanitor.AspNet.AdminUi/Components/TaskProgressionComponent.razor new file mode 100644 index 0000000..c8cab97 --- /dev/null +++ b/src/AuthJanitor.AspNet.AdminUi/Components/TaskProgressionComponent.razor @@ -0,0 +1,89 @@ + + + + +
+ + + + + + + + + Temporary Secrets +
+ + +
+ + + + + + + + + Long-Term Secrets +
+ + + + + + + +@code { + [Parameter] + public RekeyingAttemptLogger Attempt { get; set; } + + // TODO: Everything here is a hack to parse the log for indicators + // Need to move provider actions into their own objects to track this better + private const string TEST_PHASE_INDICATOR = "### Performing provider tests..."; + + private const string GENERATE_TEMPORARY_PHASE_INDICATOR = "### Retrieving/generating temporary secrets..."; + private const string DISTRIBUTE_TEMPORARY_PHASE_INDICATOR = "### Distributing temporary secrets..."; + private const string COMMIT_TEMPORARY_PHASE_INDICATOR = "### Performing commits for temporary secrets..."; + + private const string GENERATE_LONG_TERM_PHASE_INDICATOR = "### Rekeying objects and services..."; + private const string DISTRIBUTE_LONG_TERM_PHASE_INDICATOR = "### Distributing regenerated secrets..."; + private const string COMMIT_LONG_TERM_PHASE_INDICATOR = "### Performing commits..."; + private const string CLEANUP_PHASE_INDICATOR = "### Running cleanup operations..."; + private const string END_PROCESS_INDICATOR = "########## END REKEYING WORKFLOW ##########"; + + private const string PHASE_NOT_STARTED_COLOR = "fa-3x text-secondary"; + private const string PHASE_IN_PROGRESS_COLOR = "fa-3x text-warning"; + private const string PHASE_COMPLETE_COLOR = "fa-3x text-success"; + private const string PHASE_ERROR_COLOR = "fa-3x text-danger"; + + public string TestPhaseColor => GetColorFromLogLines(TEST_PHASE_INDICATOR, GENERATE_TEMPORARY_PHASE_INDICATOR); + + public string GenerateTemporaryPhaseColor => GetColorFromLogLines(GENERATE_TEMPORARY_PHASE_INDICATOR, DISTRIBUTE_TEMPORARY_PHASE_INDICATOR); + public string DistributeTemporaryPhaseColor => GetColorFromLogLines(DISTRIBUTE_TEMPORARY_PHASE_INDICATOR, COMMIT_TEMPORARY_PHASE_INDICATOR); + public string CommitTemporaryPhaseColor => GetColorFromLogLines(COMMIT_TEMPORARY_PHASE_INDICATOR, GENERATE_LONG_TERM_PHASE_INDICATOR); + + public string GenerateLongTermPhaseColor => GetColorFromLogLines(GENERATE_LONG_TERM_PHASE_INDICATOR, DISTRIBUTE_LONG_TERM_PHASE_INDICATOR); + public string DistributeLongTermPhaseColor => GetColorFromLogLines(DISTRIBUTE_LONG_TERM_PHASE_INDICATOR, COMMIT_LONG_TERM_PHASE_INDICATOR); + public string CommitLongTermPhaseColor => GetColorFromLogLines(COMMIT_LONG_TERM_PHASE_INDICATOR, CLEANUP_PHASE_INDICATOR); + + public string CleanupPhaseColor => GetColorFromLogLines(CLEANUP_PHASE_INDICATOR, END_PROCESS_INDICATOR); + public string CompletePhaseColor => GetColorFromLogLines(END_PROCESS_INDICATOR); + + protected string GetColorFromLogLines(string leftBookend, string rightBookend = null) + { + if (Attempt == null) return PHASE_NOT_STARTED_COLOR; + if (Attempt.LogString == null) return PHASE_NOT_STARTED_COLOR; + + if (Attempt.LogString.Contains(leftBookend)) + { + if (string.IsNullOrEmpty(rightBookend)) + return PHASE_COMPLETE_COLOR; + if (Attempt.LogString.Contains(rightBookend)) + return PHASE_COMPLETE_COLOR; + if (!string.IsNullOrEmpty(Attempt.OuterException)) + return PHASE_ERROR_COLOR; + return PHASE_IN_PROGRESS_COLOR; + } + return PHASE_NOT_STARTED_COLOR; + } +} \ No newline at end of file diff --git a/src/AuthJanitor.AspNet.AdminUi/Editors/ManagedSecretEditor.razor b/src/AuthJanitor.AspNet.AdminUi/Editors/ManagedSecretEditor.razor index 69ba97b..2f89c42 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Editors/ManagedSecretEditor.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Editors/ManagedSecretEditor.razor @@ -3,7 +3,7 @@ { - @((MarkupString)item.Provider.SvgImage) + @((MarkupString)item.Provider.Details.SvgImage) @item.Name diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecretDetail.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecretDetail.razor index b501019..2060703 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecretDetail.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecretDetail.razor @@ -22,7 +22,7 @@ @foreach (var item in this.Secret.Resources) { -
@((MarkupString)item.Provider.SvgImage)
+
@((MarkupString)item.Provider.Details.SvgImage)
}
@@ -65,7 +65,7 @@ - @@ -80,7 +80,7 @@ - @((MarkupString)resource.Provider.SvgImage) + @((MarkupString)resource.Provider.Details.SvgImage) @@ -135,11 +135,19 @@ - + + + @code { public ManagedSecretViewModel Secret { get; set; } = new ManagedSecretViewModel(); + + protected bool CreateModalShowing { get; set; } + protected bool DeleteModalShowing { get; set; } protected bool ContextualHelpVisible { get; set; } [Parameter] @@ -160,4 +168,14 @@ resource.ProviderConfiguration.SerializedConfiguration = resource.SerializedProviderConfiguration; })); } + + protected async Task DeleteConfirmCallback(bool result) + { + if (result) + { + await Http.AJDelete(Secret.ObjectId); + NavigationManager.NavigateTo("/managedSecrets"); + } + DeleteModalShowing = false; + } } diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecrets.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecrets.razor index 5cad1ab..c15ca33 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecrets.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/ManagedSecrets.razor @@ -69,7 +69,7 @@ {
-
@((MarkupString)resource.Provider.SvgImage)
+
@((MarkupString)resource.Provider.Details.SvgImage)
@resource.Name
diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/Providers.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/Providers.razor index 84a6bc6..a419fdb 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/Providers.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/Providers.razor @@ -16,7 +16,7 @@ Sortable="true" Filterable="true"> @@ -30,24 +30,9 @@ Sortable="true" Filterable="true" /> + Title="Features" Field="@(x => x.Details)"> @@ -77,7 +62,7 @@ @code { protected IEnumerable ProviderList { get; set; } = new List(); protected bool ContextualHelpVisible { get; set; } - + protected override async Task OnInitializedAsync() => await LoadData(); protected async Task LoadData() diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTaskDetail.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTaskDetail.razor index fb439cd..7651b49 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTaskDetail.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTaskDetail.razor @@ -1,152 +1,150 @@ @page "/rekeyingTasks/{RekeyingTaskId}" - - - - + + + + + + + + + + @Task.ManagedSecret.Name + + + + + + - - + + + Last Changed + + + @Task.ManagedSecret.LastChanged + - - + + Expires + + + @Task.Expiry + + - - + + + + + - - + + + + + + + + - - - - - - - - - - - @Task.ManagedSecret.Name - - - - - - - - - - Last Changed - - - @Task.ManagedSecret.LastChanged - - - - Expires - - - @Task.Expiry - - - - - - - - - - - - - - - - - - - @if (Task.Attempts.Any()) - { - - - - - @foreach (var attempt in Task.Attempts) - { - - @if (attempt.IsSuccessfulAttempt && attempt.AttemptFinished != default) + @if (Task.Attempts.Any()) + { + + + + + @foreach (var attempt in Task.Attempts) + { + + @if (attempt.IsComplete && attempt.IsSuccessfulAttempt) { - + } - else if (attempt.AttemptFinished != default) + else if (attempt.IsComplete && !attempt.IsSuccessfulAttempt) { - + } else { - + } - @attempt.UserDisplayName
- @attempt.AttemptStarted.ToString() -
- } -
- - @foreach (var attempt in Task.Attempts) - { - -
@attempt.LogString
- @if (!attempt.IsSuccessfulAttempt) + @attempt.UserDisplayName
+ @attempt.AttemptStarted.ToString() +
+ } +
+ + @foreach (var attempt in Task.Attempts) + { + +
@attempt.LogString
+ @if (!attempt.IsSuccessfulAttempt) { - + } -
- } -
-
-
-
- } + + } + + +
+
+ } - - - Rekeying Tasks are created, either automatically by the system as a key or secret nears its expiry, - or manually by an administrator. A Rekeying Task is associated with a single - Secret. Rekeying Tasks can have multiple attempts by different administrators - or service accounts. - - + + + Rekeying Tasks are created, either automatically by the system as a key or secret nears its expiry, + or manually by an administrator. A Rekeying Task is associated with a single + Secret. Rekeying Tasks can have multiple attempts by different administrators + or service accounts. + + + + + + + + @bind-ContextualHelpVisible="@ContextualHelpVisible" /> @using AuthJanitor.UI.Shared.ViewModels @code { public ManagedSecretViewModel Secret => Task == null ? new ManagedSecretViewModel() : Task.ManagedSecret; public RekeyingTaskViewModel Task { get; set; } = new RekeyingTaskViewModel(); - protected bool ContextualHelpVisible { get; set; } + protected bool ApproveModalShowing { get; set; } + protected bool DeleteModalShowing { get; set; } + public bool ContextualHelpVisible { get; set; } + + public RekeyingAttemptLogger Attempt { get; set; } [Parameter] public string RekeyingTaskId { get; set; } @@ -154,25 +152,53 @@ public TimeSpan DurationSoFar => DateTimeOffset.UtcNow - Secret.LastChanged.GetValueOrDefault(); protected IEnumerable _providers; - protected override async Task OnInitializedAsync() => await LoadData(); + protected override async Task OnInitializedAsync() + { + await LoadData(); + } protected async Task LoadData() { _providers = await Http.AJList(); Task = await Http.AJGet(Guid.Parse(RekeyingTaskId)); if (Task.Attempts.Any()) - SelectedAttemptTab = Task.Attempts.OrderByDescending(a => a.AttemptStarted).FirstOrDefault()?.AttemptStarted.ToString(); - - await System.Threading.Tasks.Task.WhenAll(Task.ManagedSecret.Resources.Select(async resource => { - resource.ProviderConfiguration = await Http.AJGet(resource.ProviderType); - resource.ProviderConfiguration.SerializedConfiguration = resource.SerializedProviderConfiguration; - })); + SelectedAttemptTab = Task.Attempts.OrderByDescending(a => a.AttemptStarted).FirstOrDefault()?.AttemptStarted.ToString(); + Attempt = Task.Attempts.OrderByDescending(a => a.AttemptFinished).FirstOrDefault(); + } + + //await System.Threading.Tasks.Task.WhenAll(Task.ManagedSecret.Resources.Select(async resource => + //{ + // resource.ProviderConfiguration = await Http.AJGet(resource.ProviderType); + // resource.ProviderConfiguration.SerializedConfiguration = resource.SerializedProviderConfiguration; + //})); } string SelectedAttemptTab; private void OnSelectedTabChanged(string name) { SelectedAttemptTab = name; + Attempt = Task.Attempts.FirstOrDefault(a => a.AttemptStarted.ToString() == name); + StateHasChanged(); + } + + protected async Task ApproveCallback(bool result) + { + if (result) + { + await Http.PostAsync($"/api/tasks/{Task.ObjectId}/approve", new StringContent(string.Empty)); + await LoadData(); + } + ApproveModalShowing = false; + } + + protected async Task DeleteConfirmCallback(bool result) + { + if (result) + { + await Http.AJDelete(Task.ObjectId); + NavigationManager.NavigateTo("/rekeyingTasks"); + } + DeleteModalShowing = false; } } \ No newline at end of file diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTasks.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTasks.razor index a6ec0bd..ca9610a 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTasks.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/RekeyingTasks.razor @@ -75,7 +75,7 @@ {
-
@((MarkupString)resource.Provider.SvgImage)
+
@((MarkupString)resource.Provider.Details.SvgImage)
@resource.Name
@@ -136,10 +136,13 @@ protected async Task LoadData() => Tasks = await Http.AJList(); - protected async Task ApproveCallback() + protected async Task ApproveCallback(bool result) { - await Http.PostAsync($"/api/tasks/{SelectedValue.ObjectId}/approve", new StringContent(string.Empty)); - await LoadData(); + if (result) + { + await Http.PostAsync($"/api/tasks/{SelectedValue.ObjectId}/approve", new StringContent(string.Empty)); + await LoadData(); + } ApproveModalShowing = false; } diff --git a/src/AuthJanitor.AspNet.AdminUi/Pages/ResourceDetail.razor b/src/AuthJanitor.AspNet.AdminUi/Pages/ResourceDetail.razor index d3e9d7c..7e51f55 100644 --- a/src/AuthJanitor.AspNet.AdminUi/Pages/ResourceDetail.razor +++ b/src/AuthJanitor.AspNet.AdminUi/Pages/ResourceDetail.razor @@ -10,7 +10,7 @@ @if (_provider != null) { -
@((MarkupString)_provider.SvgImage)
+
@((MarkupString)_provider.Details.SvgImage)
@_provider.Details.Name
} @@ -38,7 +38,7 @@ ProviderConfiguration="@Resource.ProviderConfiguration" />
- @@ -101,6 +101,11 @@ + + @@ -109,6 +114,7 @@ [Parameter] public string ResourceId { get; set; } + protected bool DeleteModalShowing { get; set; } protected bool ContextualHelpVisible { get; set; } public ResourceViewModel Resource { get; set; } = new ResourceViewModel(); @@ -124,4 +130,14 @@ Resource.ProviderConfiguration = await Http.AJGet(Resource.ProviderType); Resource.ProviderConfiguration.SerializedConfiguration = Resource.SerializedProviderConfiguration; } + + protected async Task DeleteConfirmCallback(bool result) + { + if (result) + { + await Http.AJDelete(Resource.ObjectId); + await LoadData(); + } + DeleteModalShowing = false; + } } \ No newline at end of file diff --git a/src/AuthJanitor.AspNet.AdminUi/wwwroot/css/AJAnimated.svg b/src/AuthJanitor.AspNet.AdminUi/wwwroot/css/AJAnimated.svg new file mode 100644 index 0000000..ea4ee4e --- /dev/null +++ b/src/AuthJanitor.AspNet.AdminUi/wwwroot/css/AJAnimated.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/src/AuthJanitor.AspNet.AdminUi/wwwroot/index.html b/src/AuthJanitor.AspNet.AdminUi/wwwroot/index.html index 012682f..f596bc7 100644 --- a/src/AuthJanitor.AspNet.AdminUi/wwwroot/index.html +++ b/src/AuthJanitor.AspNet.AdminUi/wwwroot/index.html @@ -1,4 +1,4 @@ - + @@ -33,7 +33,6 @@ - @@ -41,33 +40,26 @@
-

- 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 +