Fix provider collisions, UI improvements (#95)

* Fix provider collisions, UI improvements

* UI fixes, bolstering Storage
This commit is contained in:
Anthony Turner 2020-07-16 01:38:57 -07:00 коммит произвёл GitHub
Родитель 654cb07cce
Коммит 8ed075aeac
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
86 изменённых файлов: 1176 добавлений и 812 удалений

Просмотреть файл

@ -18,11 +18,11 @@
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="0.9.1.1" />
<PackageReference Include="Blazorise.Sidebar" Version="0.9.1.1" />
<PackageReference Include="BlazorTable" Version="1.11.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="3.2.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="3.2.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="3.2.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="3.2.0-preview3.20168.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -0,0 +1,48 @@
@{
void RenderCapabilityIcon(string icon, string templateString, bool isLit)
{
<Icon Name="@icon"
Class="@(isLit ? IconLitColor : IconUnlitColor)"
title="@GetTitle(templateString, isLit)"
Margin="Margin.Is2"></Icon>
}
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));
}

Просмотреть файл

@ -0,0 +1,89 @@
<Icon Name="FontAwesomeIcons.Flask" Class="@TestPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<div class="px-3 pt-2 pb-0 bg-light d-inline-block border border-secondary">
<Icon Name="FontAwesomeIcons.Database" Class="@GenerateTemporaryPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.CloudUploadAlt" Class="@DistributeTemporaryPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.Save" Class="@CommitTemporaryPhaseColor" />
<small class="d-block mt-0 mb-1 text-gray-800">Temporary Secrets</small>
</div>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<div class="px-3 pt-2 pb-0 bg-light d-inline-block border border-secondary">
<Icon Name="FontAwesomeIcons.Database" Class="@GenerateLongTermPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.CloudUploadAlt" Class="@DistributeLongTermPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.Save" Class="@CommitLongTermPhaseColor" />
<small class="d-block mt-0 mb-1 text-gray-800">Long-Term Secrets</small>
</div>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.Broom" Class="@CleanupPhaseColor" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-dark" />
<Icon Name="FontAwesomeIcons.FlagCheckered" Class="@CompletePhaseColor" />
@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;
}
}

Просмотреть файл

@ -3,7 +3,7 @@
{
<Row>
<Column ColumnSize="ColumnSize.Is2" >
@((MarkupString)item.Provider.SvgImage)
@((MarkupString)item.Provider.Details.SvgImage)
</Column>
<Column ColumnSize="ColumnSize.Is10">
<Check TValue="bool" CheckedChanged="@(v => UpdateObject(item.ObjectId, v))"><strong>@item.Name</strong></Check>

Просмотреть файл

@ -22,7 +22,7 @@
<Column ColumnSize="ColumnSize.Is6.Is9.OnWidescreen" Padding="Padding.Is2" Class="border-left">
@foreach (var item in this.Secret.Resources)
{
<div style="width:1em;" class="mr-3 float-left" title="@item.Name">@((MarkupString)item.Provider.SvgImage)</div>
<div style="width:1em;" class="mr-3 float-left" title="@item.Name">@((MarkupString)item.Provider.Details.SvgImage)</div>
}
</Column>
</Row>
@ -65,7 +65,7 @@
</Button>
</Column>
<Column ColumnSize="ColumnSize.Is6" Class="text-center">
<Button Disabled="true" Color="Color.Danger">
<Button Color="Color.Danger" Clicked="@(() => { DeleteModalShowing = true; })">
<Icon Name="FontAwesomeIcons.Trash" Margin="Margin.Is3.FromRight" />
Delete Secret
</Button>
@ -80,7 +80,7 @@
<CardBody>
<Row>
<Column ColumnSize="ColumnSize.Is3" Class="providerImage">
@((MarkupString)resource.Provider.SvgImage)
@((MarkupString)resource.Provider.Details.SvgImage)
</Column>
<Column ColumnSize="ColumnSize.Is9">
<Heading Size="HeadingSize.Is3">
@ -135,11 +135,19 @@
</AuthJanitor.UI.Components.HelpSlideInComponent>
</Container>
<SystemWideFooter RefreshDataClicked="@(() => LoadData())"
@bind-ContextualHelpVisible="@ContextualHelpVisible"/>
<DeleteConfirmationModal @bind-Visible="@DeleteModalShowing"
ObjectId="@Secret.ObjectId"
ObjectName="@Secret.Name"
ResultClicked="@DeleteConfirmCallback" />
<SystemWideFooter RefreshDataClicked="@(() => LoadData())"
@bind-ContextualHelpVisible="@ContextualHelpVisible" />
@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<ManagedSecretViewModel>(Secret.ObjectId);
NavigationManager.NavigateTo("/managedSecrets");
}
DeleteModalShowing = false;
}
}

Просмотреть файл

@ -69,7 +69,7 @@
{
<ListGroupItem Padding="Padding.Is1.OnAll" Class="justify-content-between align-items-center d-flex">
<div>
<div style="width:1em;" class="mr-2 float-left">@((MarkupString)resource.Provider.SvgImage)</div>
<div style="width:1em;" class="mr-2 float-left">@((MarkupString)resource.Provider.Details.SvgImage)</div>
@resource.Name
</div>
<ColoredRiskScore ShowRiskText="false" Value="@resource.RiskScore" />

Просмотреть файл

@ -16,7 +16,7 @@
Sortable="true" Filterable="true">
<Template>
<span class="float-left mr-2" style="width:1em;">
@((MarkupString)context.SvgImage)
@((MarkupString)context.Details.SvgImage)
</span>
@context.Details.Name
</Template>
@ -30,24 +30,9 @@
Sortable="true" Filterable="true" />
<BlazorTable.Column TableItem="LoadedProviderViewModel"
Title="Features" Field="@(x => x.Details.Features)">
Title="Features" Field="@(x => x.Details)">
<Template>
<Icon Name="FontAwesomeIcons.Flask"
Class="@(context.Details.Features.HasFlag(ProviderFeatureFlags.IsTestable) ? "text-success" : "text-dark")"
title="@("Provider " + (context.Details.Features.HasFlag(ProviderFeatureFlags.IsTestable) ? "Supports" : "Does Not Support") + " Pre-Testing")"
Margin="Margin.Is2" />
<Icon Name="FontAwesomeIcons.SyncAlt"
Class="@(context.Details.Features.HasFlag(ProviderFeatureFlags.CanRotateWithoutDowntime) ? "text-success" : "text-dark")"
title="@("Provider " + (context.Details.Features.HasFlag(ProviderFeatureFlags.CanRotateWithoutDowntime) ? "Can" : "Cannot") + " Rotate Without Downtime")"
Margin="Margin.Is2" />
<Icon Name="FontAwesomeIcons.LayerGroup"
Class="@(context.Details.Features.HasFlag(ProviderFeatureFlags.HasCandidateSelection) ? "text-success" : "text-dark")"
title="@("Provider " + (context.Details.Features.HasFlag(ProviderFeatureFlags.HasCandidateSelection) ? "Supports" : "Does Not Support") + " Candidate Selection")"
Margin="Margin.Is2" />
<Icon Name="FontAwesomeIcons.Key"
Class="@(context.Details.Features.HasFlag(ProviderFeatureFlags.SupportsSecondaryKey) ? "text-success" : "text-dark")"
title="@("Provider " + (context.Details.Features.HasFlag(ProviderFeatureFlags.SupportsSecondaryKey) ? "Supports" : "Does Not Support") + " Use of a Secondary Key")"
Margin="Margin.Is2" />
<ProviderCapabilitiesComponent Provider="@context" />
</Template>
</BlazorTable.Column>
@ -77,7 +62,7 @@
@code {
protected IEnumerable<LoadedProviderViewModel> ProviderList { get; set; } = new List<LoadedProviderViewModel>();
protected bool ContextualHelpVisible { get; set; }
protected override async Task OnInitializedAsync() => await LoadData();
protected async Task LoadData()

Просмотреть файл

@ -1,152 +1,150 @@
@page "/rekeyingTasks/{RekeyingTaskId}"
<Container Fluid="true" Style="position:relative;">
<AuthJanitor.UI.Components.BreadcrumbRow Category="Manage"
PageGroup="Rekeying Tasks"
PageGroupLink="/rekeyingTasks"
OptionalObjectName="@Task.ManagedSecret.Name" />
<Row Margin="Margin.Is2.OnY">
<Column Class="text-center">
<Icon Name="FontAwesomeIcons.Flask" Class="fa-3x text-success" />
<AuthJanitor.UI.Components.BreadcrumbRow Category="Manage"
PageGroup="Rekeying Tasks"
PageGroupLink="/rekeyingTasks"
OptionalObjectName="@Task.ManagedSecret.Name" />
<Row Margin="Margin.Is2.OnY">
<Column Class="text-center d-flex align-items-center justify-content-center">
<TaskProgressionComponent Attempt="@Attempt" />
</Column>
</Row>
<Row Padding="Padding.Is2.FromBottom" Class="border-bottom">
<Column Class="text-center">
<DisplayHeading Size="DisplayHeadingSize.Is4">
@Task.ManagedSecret.Name
<a href="@($"/managedSecrets/{Task.ManagedSecret.ObjectId}")" class="h5">
<Icon Name="FontAwesomeIcons.Link" />
</a>
</DisplayHeading>
</Column>
</Row>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-success" />
<Icon Name="FontAwesomeIcons.Database" Class="fa-3x text-success" />
<Row Class="border-bottom">
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left bg-light">
<Heading Size="HeadingSize.Is5" Margin="Margin.IsAuto.OnY">Last Changed</Heading>
</Column>
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left">
<Paragraph Margin="Margin.IsAuto.OnY">@Task.ManagedSecret.LastChanged</Paragraph>
</Column>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-success" />
<Icon Name="FontAwesomeIcons.Cogs" Class="fa-3x text-success" />
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left bg-light">
<Heading Size="HeadingSize.Is5" Margin="Margin.IsAuto.OnY">Expires</Heading>
</Column>
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left">
<Paragraph Margin="Margin.IsAuto.OnY">@Task.Expiry</Paragraph>
</Column>
</Row>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-success" />
<Icon Name="FontAwesomeIcons.EllipsisH" Class="fa-3x text-success" />
<Row Class="border-bottom">
<Column Padding="Padding.Is0">
<AuthJanitor.UI.Components.ColoredProgressBar Value="@Task.ManagedSecret.ExpiryPercent" />
</Column>
</Row>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-primary" />
<Icon Name="FontAwesomeIcons.Database" Class="fa-3x text-primary" />
<Row Padding="Padding.Is3.OnY" Class="shadow-sm bg-light">
<Column ColumnSize="ColumnSize.Is6" Class="text-center">
<Button Color="Color.Success" Clicked="@(() => { ApproveModalShowing = true; })">
<Icon Name="FontAwesomeIcons.Check" Margin="Margin.Is3.FromRight" />
Approve Rekeying Task
</Button>
</Column>
<Column ColumnSize="ColumnSize.Is6" Class="text-center">
<Button Color="Color.Danger" Clicked="@(() => { DeleteModalShowing = true; })">
<Icon Name="FontAwesomeIcons.Trash" Margin="Margin.Is3.FromRight" />
Delete Rekeying Task
</Button>
</Column>
</Row>
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-secondary" />
<Icon Name="FontAwesomeIcons.Cogs" Class="fa-3x text-secondary" />
<Icon Name="FontAwesomeIcons.ArrowRight" Margin="Margin.Is2.FromBottom.Is2.OnX" Class="text-secondary" />
<Icon Name="FontAwesomeIcons.FlagCheckered" Class="fa-3x text-secondary" />
</Column>
</Row>
<Row Padding="Padding.Is2.FromBottom" Class="border-bottom">
<Column Class="text-center">
<DisplayHeading Size="DisplayHeadingSize.Is4">
@Task.ManagedSecret.Name
<a href="@($"/managedSecrets/{Task.ManagedSecret.ObjectId}")" class="h5">
<Icon Name="FontAwesomeIcons.Link" />
</a>
</DisplayHeading>
</Column>
</Row>
<Row Class="border-bottom">
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left bg-light">
<Heading Size="HeadingSize.Is5" Margin="Margin.IsAuto.OnY">Last Changed</Heading>
</Column>
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left">
<Paragraph Margin="Margin.IsAuto.OnY">@Task.ManagedSecret.LastChanged</Paragraph>
</Column>
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left bg-light">
<Heading Size="HeadingSize.Is5" Margin="Margin.IsAuto.OnY">Expires</Heading>
</Column>
<Column ColumnSize="ColumnSize.Is6.Is3.OnWidescreen" Padding="Padding.Is2" Class="border-left">
<Paragraph Margin="Margin.IsAuto.OnY">@Task.Expiry</Paragraph>
</Column>
</Row>
<Row Class="border-bottom">
<Column Padding="Padding.Is0">
<AuthJanitor.UI.Components.ColoredProgressBar Value="@Task.ManagedSecret.ExpiryPercent" />
</Column>
</Row>
<Row Padding="Padding.Is3.OnY" Class="shadow-sm bg-light">
<Column ColumnSize="ColumnSize.Is6" Class="text-center">
<Button Color="Color.Success" Disabled="true">
<Icon Name="FontAwesomeIcons.Check" Margin="Margin.Is3.FromRight" />
Approve Rekeying Task
</Button>
</Column>
<Column ColumnSize="ColumnSize.Is6" Class="text-center">
<Button Color="Color.Danger" Disabled="true">
<Icon Name="FontAwesomeIcons.Trash" Margin="Margin.Is3.FromRight" />
Delete Rekeying Task
</Button>
</Column>
</Row>
@if (Task.Attempts.Any())
{
<Row Margin="Margin.Is3.FromTop">
<Column ColumnSize="ColumnSize.Is12">
<Tabs SelectedTab="@SelectedAttemptTab" SelectedTabChanged="@OnSelectedTabChanged">
<Items>
@foreach (var attempt in Task.Attempts)
{
<Tab Name="@attempt.AttemptStarted.ToString()">
@if (attempt.IsSuccessfulAttempt && attempt.AttemptFinished != default)
@if (Task.Attempts.Any())
{
<Row Margin="Margin.Is3.FromTop">
<Column ColumnSize="ColumnSize.Is12">
<Tabs SelectedTab="@SelectedAttemptTab" SelectedTabChanged="@OnSelectedTabChanged">
<Items>
@foreach (var attempt in Task.Attempts)
{
<Tab Name="@attempt.AttemptStarted.ToString()">
@if (attempt.IsComplete && attempt.IsSuccessfulAttempt)
{
<Icon Name="FontAwesomeIcons.CheckCircle" Class="text-success" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
<Icon Name="FontAwesomeIcons.CheckCircle" Class="text-success" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
}
else if (attempt.AttemptFinished != default)
else if (attempt.IsComplete && !attempt.IsSuccessfulAttempt)
{
<Icon Name="FontAwesomeIcons.TimesCircle" Class="text-danger" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
<Icon Name="FontAwesomeIcons.TimesCircle" Class="text-danger" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
}
else
{
<Icon Name="FontAwesomeIcons.PlayCircle" Class="text-info" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
<Icon Name="FontAwesomeIcons.PlayCircle" Class="text-info" Margin="Margin.IsAuto.OnY.Is1.FromRight" />
}
@attempt.UserDisplayName<br />
<small>@attempt.AttemptStarted.ToString()</small>
</Tab>
}
</Items>
<Content>
@foreach (var attempt in Task.Attempts)
{
<TabPanel Name="@attempt.AttemptStarted.ToString()">
<pre><code>@attempt.LogString</code></pre>
@if (!attempt.IsSuccessfulAttempt)
@attempt.UserDisplayName<br />
<small>@attempt.AttemptStarted.ToString()</small>
</Tab>
}
</Items>
<Content>
@foreach (var attempt in Task.Attempts)
{
<TabPanel Name="@attempt.AttemptStarted.ToString()">
<pre><code>@attempt.LogString</code></pre>
@if (!attempt.IsSuccessfulAttempt)
{
<div class="alert alert-danger" role="alert">
<h4 class="text-danger">
Exception thrown!
</h4>
<p>
<pre><code>@attempt.OuterException</code></pre>
</p>
</div>
<div class="alert alert-danger" role="alert">
<h4 class="text-danger">
Exception thrown!
</h4>
<p>
<pre><code>@attempt.OuterException</code></pre>
</p>
</div>
}
</TabPanel>
}
</Content>
</Tabs>
</Column>
</Row>
}
</TabPanel>
}
</Content>
</Tabs>
</Column>
</Row>
}
<AuthJanitor.UI.Components.HelpSlideInComponent Title="Rekeying Tasks"
Icon="@FontAwesomeIcons.Tasks"
@bind-Visible="@ContextualHelpVisible">
<Paragraph>
<strong>Rekeying Tasks</strong> are created, either automatically by the system as a key or secret nears its expiry,
or manually by an administrator. A <strong>Rekeying Task</strong> is associated with a <em>single</em>
<strong>Secret</strong>. <strong>Rekeying Tasks</strong> can have multiple attempts by different administrators
or service accounts.
</Paragraph>
</AuthJanitor.UI.Components.HelpSlideInComponent>
<AuthJanitor.UI.Components.HelpSlideInComponent Title="Rekeying Tasks"
Icon="@FontAwesomeIcons.Tasks"
@bind-Visible="@ContextualHelpVisible">
<Paragraph>
<strong>Rekeying Tasks</strong> are created, either automatically by the system as a key or secret nears its expiry,
or manually by an administrator. A <strong>Rekeying Task</strong> is associated with a <em>single</em>
<strong>Secret</strong>. <strong>Rekeying Tasks</strong> can have multiple attempts by different administrators
or service accounts.
</Paragraph>
</AuthJanitor.UI.Components.HelpSlideInComponent>
</Container>
<DataModal @bind-Visible="@ApproveModalShowing"
Title="Approve Task"
YesButton="Approve"
NoButton="Cancel"
ResultClicked="@ApproveCallback">
<ApprovalSummary Task="@Task" />
</DataModal>
<DeleteConfirmationModal @bind-Visible="@DeleteModalShowing"
ObjectId="@Task.ObjectId"
ObjectName="@Task.ObjectId.ToString()"
ResultClicked="@DeleteConfirmCallback" />
<SystemWideFooter RefreshDataClicked="@(() => LoadData())"
@bind-ContextualHelpVisible="@ContextualHelpVisible"/>
@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<LoadedProviderViewModel> _providers;
protected override async Task OnInitializedAsync() => await LoadData();
protected override async Task OnInitializedAsync()
{
await LoadData();
}
protected async Task LoadData()
{
_providers = await Http.AJList<LoadedProviderViewModel>();
Task = await Http.AJGet<RekeyingTaskViewModel>(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<ProviderConfigurationViewModel>(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<ProviderConfigurationViewModel>(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<RekeyingTaskViewModel>(Task.ObjectId);
NavigationManager.NavigateTo("/rekeyingTasks");
}
DeleteModalShowing = false;
}
}

Просмотреть файл

@ -75,7 +75,7 @@
{
<ListGroupItem Padding="Padding.Is1.OnAll" Class="justify-content-between align-items-center d-flex">
<div>
<div style="width:1em;" class="mr-2 float-left">@((MarkupString)resource.Provider.SvgImage)</div>
<div style="width:1em;" class="mr-2 float-left">@((MarkupString)resource.Provider.Details.SvgImage)</div>
@resource.Name
</div>
<ColoredRiskScore ShowRiskText="false" Value="@resource.RiskScore" />
@ -136,10 +136,13 @@
protected async Task LoadData() => Tasks = await Http.AJList<RekeyingTaskViewModel>();
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;
}

Просмотреть файл

@ -10,7 +10,7 @@
@if (_provider != null)
{
<Column Class="bg-light" Margin="Margin.IsAuto.OnY" ColumnSize="ColumnSize.Is1">
<div class="providerImage my-2 mx-auto">@((MarkupString)_provider.SvgImage)</div>
<div class="providerImage my-2 mx-auto">@((MarkupString)_provider.Details.SvgImage)</div>
<div class="d-none d-sm-block text-center border-top py-2 small">@_provider.Details.Name</div>
</Column>
}
@ -38,7 +38,7 @@
ProviderConfiguration="@Resource.ProviderConfiguration" />
</Column>
<Column ColumnSize="ColumnSize.Is4" Class="text-center">
<Button Color="Color.Danger" Disabled="true">
<Button Color="Color.Danger" Clicked="@(() => { DeleteModalShowing = true; })">
<Icon Name="FontAwesomeIcons.Trash" Margin="Margin.Is3.FromRight" />
Delete Resource
</Button>
@ -101,6 +101,11 @@
</Container>
<DeleteConfirmationModal @bind-Visible="@DeleteModalShowing"
ObjectId="@Resource.ObjectId"
ObjectName="@Resource.Name"
ResultClicked="@DeleteConfirmCallback" />
<SystemWideFooter RefreshDataClicked="@(() => LoadData())"
@bind-ContextualHelpVisible="@ContextualHelpVisible"/>
@ -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<ProviderConfigurationViewModel>(Resource.ProviderType);
Resource.ProviderConfiguration.SerializedConfiguration = Resource.SerializedProviderConfiguration;
}
protected async Task DeleteConfirmCallback(bool result)
{
if (result)
{
await Http.AJDelete<ResourceViewModel>(Resource.ObjectId);
await LoadData();
}
DeleteModalShowing = false;
}
}

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

После

Ширина:  |  Высота:  |  Размер: 83 KiB

Просмотреть файл

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<!-- Copyright (c) Microsoft Corporation. -->
<!-- Licensed under the MIT license. -->
<html>
@ -33,7 +33,6 @@
<link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
<link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet" />
<link href="_content/AuthJanitor.AspNet.UI.Razor/style.css" rel="stylesheet" />
<link href="css/style.css" rel="stylesheet" />
</head>
@ -41,33 +40,26 @@
<body>
<app>
<style type="text/css">
/*
Logo font: Berlin Sans Demi
Loader BG: #305a70
SVG has 2 rectangles which must match bg color for effects
Note: Need to fix vector clip issue on "A" lid in animated logo
*/
body {
background-color: #305a70;
overflow: hidden;
}
.insetText {
background-color: #103a50;
color: transparent;
text-shadow: 2px 2px 1px rgba(255,255,255,0.3);
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
div.ajAnimated {
width: 8vw;
height: auto;
margin: 0 auto;
}
</style>
<div class="fixed-top text-center mt-3">
<h1 class="h1 insetText" style="
font-size: 80px;
font-weight: bold;
margin: 25px auto;">
AuthJanitor
</h1>
<div class="loader">
<div style="position: relative; width: 100%; height: 100%;">
<div style="position: absolute; top: 0; bottom: 0; left: 0; right: 0; text-align: center; margin-top: calc(50% - 45pt); z-index: 100; color: #ccc; font-size: 40pt;">
<i class="fa fa-2x fas fa-user-lock"></i>
</div>
</div>
<div class="ajAnimated">
<object class="img-fluid" type="image/svg+xml" data="css/AJAnimated.svg"></object>
</div>
</div>
</app>
@ -77,7 +69,7 @@
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://use.fontawesome.com/releases/v5.13.0/js/all.js"></script>
<!--<script src="https://use.fontawesome.com/releases/v5.13.0/js/all.js"></script>-->
<script src="_content/Blazorise/blazorise.js"></script>
<script src="_content/Blazorise.Bootstrap/blazorise.bootstrap.js"></script>

Просмотреть файл

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

Просмотреть файл

@ -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<ManagedSecret> _managedSecrets;
private readonly IDataStore<RekeyingTask> _rekeyingTasks;
private readonly IDataStore<Resource> _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<ManagedSecret> managedSecrets,
IDataStore<RekeyingTask> rekeyingTasks,
IDataStore<Resource> 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<AccessTokenCredential>(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<ManagedSecret> _managedSecrets;
private readonly IDataStore<RekeyingTask> _rekeyingTasks;
private readonly IDataStore<Resource> _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<ManagedSecret> managedSecrets,
IDataStore<RekeyingTask> rekeyingTasks,
IDataStore<Resource> 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<AccessTokenCredential>(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\\-\\._~\\+\\/]+=*", "<<REDACTED BEARER TOKEN>>");
//myAttempt.OuterException = $"{ex.Message}{Environment.NewLine}{ex.StackTrace}";
task.RekeyingInProgress = false;
task.RekeyingFailed = true;
await _rekeyingTasks.Update(task, cancellationToken);
}
}
}

Просмотреть файл

@ -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<ProviderCapabilities> GetProviderCapabilities(Type providerType)
{
var capabilities = new List<ProviderCapabilities>();
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<ProviderManagerService>();

Просмотреть файл

@ -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<ProviderCapabilities> Capabilities { get; set; } = new[] { ProviderCapabilities.None };
public string AssemblyVersion { get; set; }
}
}

Просмотреть файл

@ -45,7 +45,6 @@ namespace AuthJanitor.UI.Shared.ViewModels
var deserialized = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(value);
foreach (var item in ConfigurationItems)
{
System.Console.WriteLine($"{item.Name} => {deserialized[item.Name]} ({item.InputType})");
if (deserialized.ContainsKey(item.Name))
{
switch (item.InputType)

Просмотреть файл

@ -8,8 +8,8 @@
<ItemGroup>
<PackageReference Include="AuthenticodeExaminer" Version="0.3.0" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
</ItemGroup>
</Project>

Просмотреть файл

@ -10,21 +10,10 @@ namespace AuthJanitor.Providers
/// </summary>
public interface IApplicationLifecycleProvider : IAuthJanitorProvider
{
/// <summary>
/// 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)
/// </summary>
Task BeforeRekeying(List<RegeneratedSecret> temporaryUseSecrets);
/// <summary>
/// Call to commit the newly generated secret(s)
/// </summary>
Task CommitNewSecrets(List<RegeneratedSecret> newSecrets);
/// <summary>
/// Call after all new keys have been committed
/// </summary>
Task AfterRekeying();
Task DistributeLongTermSecretValues(List<RegeneratedSecret> newSecretValues);
}
/// <summary>
@ -33,26 +22,9 @@ namespace AuthJanitor.Providers
public abstract class ApplicationLifecycleProvider<TProviderConfiguration> : AuthJanitorProvider<TProviderConfiguration>, IApplicationLifecycleProvider
where TProviderConfiguration : AuthJanitorProviderConfiguration
{
/// <summary>
/// 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)
/// </summary>
public virtual Task BeforeRekeying(List<RegeneratedSecret> temporaryUseSecrets)
{
return Task.FromResult(true);
}
/// <summary>
/// Call to commit the newly generated secret(s)
/// </summary>
public abstract Task CommitNewSecrets(List<RegeneratedSecret> newSecrets);
/// <summary>
/// Call after all new keys have been committed
/// </summary>
public virtual Task AfterRekeying()
{
return Task.FromResult(true);
}
public abstract Task DistributeLongTermSecretValues(List<RegeneratedSecret> newSecretValues);
}
}

Просмотреть файл

@ -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
/// </summary>
AccessTokenCredential Credential { get; set; }
/// <summary>
/// Test if the current credentials can execute an Extension
/// </summary>
/// <returns></returns>
Task Test();
/// <summary>
/// Get a text description of the action which is taken by the Extension
/// </summary>
@ -50,6 +42,8 @@ namespace AuthJanitor.Providers
/// Get the Provider's metadata
/// </summary>
ProviderAttribute ProviderMetadata => GetType().GetCustomAttribute<ProviderAttribute>();
int GenerateResourceIdentifierHashCode();
}
/// <summary>
@ -77,15 +71,6 @@ namespace AuthJanitor.Providers
/// </summary>
public AccessTokenCredential Credential { get; set; }
/// <summary>
/// Test if the current credentials can execute an Extension
/// </summary>
/// <returns></returns>
public virtual Task Test()
{
return Task.FromResult(true);
}
/// <summary>
/// Get a text description of the action which is taken by the Provider
/// </summary>
@ -104,5 +89,7 @@ namespace AuthJanitor.Providers
/// </summary>
/// <returns></returns>
public virtual IList<RiskyConfigurationItem> GetRisks() => new List<RiskyConfigurationItem>();
public int GenerateResourceIdentifierHashCode() => Configuration.GenerateResourceIdentifierHashCode();
}
}

Просмотреть файл

@ -12,5 +12,12 @@ namespace AuthJanitor.Providers
/// RegeneratedSecrets entering an ApplicationLifecycleProvider
/// </summary>
public string UserHint { get; set; }
/// <summary>
/// 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.
/// </summary>
public abstract int GenerateResourceIdentifierHashCode();
}
}

Просмотреть файл

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace AuthJanitor.Providers.Capabilities
{
public interface IAuthJanitorCapability : IAuthJanitorProvider { }
}

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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<RegeneratedSecret> secretValues);
}
}

Просмотреть файл

@ -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<List<AuthJanitorProviderConfiguration>> EnumerateResourceCandidates(AuthJanitorProviderConfiguration baseConfig);
}
}

Просмотреть файл

@ -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<RegeneratedSecret> GenerateTemporarySecretValue();
}
}

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -37,11 +37,6 @@ namespace AuthJanitor.Providers
/// </summary>
public ProviderAttribute Details { get; set; }
/// <summary>
/// Provider SVG logo image
/// </summary>
public string SvgImage { get; set; }
/// <summary>
/// NET Assembly where the Provider was loaded from
/// </summary>

Просмотреть файл

@ -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
/// </summary>
public string Description { get; set; }
/// <summary>
/// Features supported by this Provider
/// </summary>
public ProviderFeatureFlags Features { get; set; }
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ProviderImageAttribute : Attribute
{
/// <summary>
/// SVG logo image for Provider
/// </summary>
public string SvgImage { get; set; }
public ProviderImageAttribute(string svgImage) =>
SvgImage = svgImage;
}
}

Просмотреть файл

@ -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<ProviderAttribute>(),
SvgImage = type.GetCustomAttribute<ProviderImageAttribute>()?.SvgImage
Details = type.GetCustomAttribute<ProviderAttribute>()
})
.ToList()
.AsReadOnly();
@ -97,63 +97,72 @@ namespace AuthJanitor.Providers
IEnumerable<IAuthJanitorProvider> providers)
{
logger.LogInformation("########## BEGIN REKEYING WORKFLOW ##########");
var rkoProviders = providers.OfType<IRekeyableObjectProvider>().ToList();
var alcProviders = providers.OfType<IApplicationLifecycleProvider>().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<ICanRunSanityTests>(),
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<RegeneratedSecret>();
await PerformProviderActions(
await PerformActionsInParallel(
logger,
rkoProviders,
p => p.GetSecretToUseDuringRekeying()
.ContinueWith(t =>
{
if (t.Result != null)
{
temporarySecrets.Add(t.Result);
}
}),
providers.OfType<ICanGenerateTemporarySecretValue>(),
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<ICanDistributeTemporarySecretValues>()
.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<RegeneratedSecret>();
await PerformProviderActions(
logger.LogInformation("### Performing commits for temporary secrets...");
await PerformActionsInParallel(
logger,
rkoProviders,
providers.OfType<ICanPerformUnifiedCommitForTemporarySecretValues>()
.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<RegeneratedSecret>();
await PerformActionsInParallel(
logger,
providers.OfType<IRekeyableObjectProvider>(),
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<IApplicationLifecycleProvider>()
.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<ICanPerformUnifiedCommit>()
.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<ICanCleanup>()
.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<TProviderType>(
private static async Task PerformActionsInSerial<TProviderType>(
ILogger logger,
IEnumerable<TProviderType> providers,
Func<TProviderType, Task> 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<TProviderType>(
ILogger logger,
IEnumerable<IGrouping<int, TProviderType>> providers,
Func<TProviderType, Task> 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<TProviderType>(
ILogger logger,
IEnumerable<TProviderType> providers,
Func<TProviderType, Task> 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);
}
}
}
}

Просмотреть файл

@ -7,25 +7,12 @@ namespace AuthJanitor.Providers
{
public interface IRekeyableObjectProvider : IAuthJanitorProvider
{
/// <summary>
/// Call before Rekeying occurs to get a secondary secret which will continue
/// to work while Rekeying is taking place (if any).
/// </summary>
/// <returns></returns>
Task<RegeneratedSecret> GetSecretToUseDuringRekeying();
/// <summary>
/// Call when ready to rekey a given RekeyableService.
/// </summary>
/// <param name="requestedValidPeriod">Requested period of validity for new key/secret</param>
/// <returns></returns>
Task<RegeneratedSecret> Rekey(TimeSpan requestedValidPeriod);
/// <summary>
/// Call when the ConsumingApplication has been moved to the RegeneratedKey (from Rekey())
/// </summary>
/// <returns></returns>
Task OnConsumingApplicationSwapped();
}
/// <summary>
@ -33,30 +20,11 @@ namespace AuthJanitor.Providers
/// </summary>
public abstract class RekeyableObjectProvider<TConfiguration> : AuthJanitorProvider<TConfiguration>, IRekeyableObjectProvider where TConfiguration : AuthJanitorProviderConfiguration
{
/// <summary>
/// Call before Rekeying occurs to get a secondary secret which will continue
/// to work while Rekeying is taking place (if any).
/// </summary>
public virtual async Task<RegeneratedSecret> GetSecretToUseDuringRekeying()
{
await Task.Yield();
return null;
}
/// <summary>
/// Call when ready to rekey a given RekeyableService.
/// </summary>
/// <param name="requestedValidPeriod">Requested period of validity for new key/secret</param>
/// <returns></returns>
public abstract Task<RegeneratedSecret> Rekey(TimeSpan requestedValidPeriod);
/// <summary>
/// Call when the ConsumingApplication has been moved to the RegeneratedKey (from Rekey())
/// </summary>
/// <returns></returns>
public virtual Task OnConsumingApplicationSwapped()
{
return Task.FromResult(true);
}
}
}

Просмотреть файл

@ -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; }

Просмотреть файл

@ -7,10 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="3.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.7" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Storage" Version="4.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AuthJanitor.AspNet\AuthJanitor.AspNet.Core.csproj" />

Просмотреть файл

@ -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<IActionResult> Add([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "access")] AuthJanitorAuthorizedUser newAuthorizedUserRole)
public async Task<IActionResult> Add([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "access")] string userJson) //AuthJanitorAuthorizedUser newAuthorizedUserRole)
{
if (!_identityService.CurrentUserHasRole(AuthJanitorRoles.GlobalAdmin)) return new UnauthorizedResult();
var newAuthorizedUserRole = JsonConvert.DeserializeObject<AuthJanitorAuthorizedUser>(userJson);
return await _managementService.AddAuthorizedUser(newAuthorizedUserRole.UPN, newAuthorizedUserRole.RoleValue);
}

Просмотреть файл

@ -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<IActionResult> Create([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets")] ManagedSecretViewModel inputSecret, CancellationToken cancellationToken)
public async Task<IActionResult> Create([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "managedSecrets")] string secretJson, //ManagedSecretViewModel inputSecret,
CancellationToken cancellationToken)
{
var inputSecret = JsonConvert.DeserializeObject<ManagedSecretViewModel>(secretJson);
return await _service.Create(inputSecret, cancellationToken);
}
@ -53,9 +56,10 @@ namespace AuthJanitor.Functions
[FunctionName("ManagedSecrets-Update")]
public async Task<IActionResult> 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<ManagedSecretViewModel>(secretJson);
return await _service.Update(inputSecret, Guid.Parse(secretId), cancellationToken);
}
}

Просмотреть файл

@ -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
{
/// <summary>
/// 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.
/// </summary>
public class Resources
{
private readonly ResourcesService _service;
public Resources(ResourcesService service)
{
_service = service;
}
[FunctionName("Resources-Create")]
public async Task<IActionResult> 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<IActionResult> List([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources")] HttpRequest req, CancellationToken cancellationToken)
{
return await _service.List(req, cancellationToken);
}
[FunctionName("Resources-Get")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Update(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources/{resourceId}")] ResourceViewModel resource,
HttpRequest req,
string resourceId, CancellationToken cancellationToken)
namespace AuthJanitor.Functions
{
/// <summary>
/// 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.
/// </summary>
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<IActionResult> Create(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources")] string resourceJson, /*ResourceViewModel resource,*/
HttpRequest req, CancellationToken cancellationToken)
{
var resource = JsonConvert.DeserializeObject<ResourceViewModel>(resourceJson);
return await _service.Create(resource, req, cancellationToken);
}
[FunctionName("Resources-List")]
public async Task<IActionResult> List([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "resources")] HttpRequest req, CancellationToken cancellationToken)
{
return await _service.List(req, cancellationToken);
}
[FunctionName("Resources-Get")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> Update(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resources/{resourceId}")] string resourceJson, //ResourceViewModel resource,
HttpRequest req,
string resourceId, CancellationToken cancellationToken)
{
var resource = JsonConvert.DeserializeObject<ResourceViewModel>(resourceJson);
return await _service.Update(resource, req, Guid.Parse(resourceId), cancellationToken);
}
}
}

Просмотреть файл

@ -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();
}

Просмотреть файл

@ -1,5 +1,6 @@
{
"version": "2.0",
"functionTimeout": "00:10:00",
"extensions": {
"http": {
"routePrefix": "api",

Просмотреть файл

@ -7,9 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.7" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions" Version="3.0.6" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.Http" Version="3.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AuthJanitor.AspNet\AuthJanitor.AspNet.Core.csproj" />

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.2" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.4.4" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.4">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Просмотреть файл

@ -5,8 +5,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
<PackageReference Include="SendGrid" Version="9.15.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
<PackageReference Include="SendGrid" Version="9.18.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AuthJanitor.Core\AuthJanitor.Core.csproj" />

Просмотреть файл

@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
<PackageReference Include="Microsoft.Graph" Version="3.8.0" />
<PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.5" />
</ItemGroup>

Просмотреть файл

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AuthJanitor.Core\AuthJanitor.Core.csproj" />

Просмотреть файл

@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.AppService.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.AppService.Fluent" Version="1.34.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -16,9 +16,7 @@ namespace AuthJanitor.Providers.AppServices.Functions
/// </summary>
[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<AppSettingConfiguration, IFunctionApp>
{
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<RegeneratedSecret> 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<IFunctionApp> GetResourceCollection(IAzure azure) => azure.AppServices.FunctionApps;

Просмотреть файл

@ -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<ConnectionStringConfiguration, IFunctionApp>
{
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<IFunctionApp> 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.";
}
}

Просмотреть файл

@ -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<FunctionKeyConfiguration, IFunctionApp>
SvgImage = ProviderImages.FUNCTIONS_SVG)]
public class FunctionKeyRekeyableObjectProvider :
AzureRekeyableObjectProvider<FunctionKeyConfiguration, IFunctionApp>,
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<RegeneratedSecret> GetSecretToUseDuringRekeying() => Task.FromResult<RegeneratedSecret>(null);
public override Task OnConsumingApplicationSwapped() => Task.FromResult(0);
protected override ISupportsGettingByResourceGroup<IFunctionApp> GetResourceCollection(IAzure azure) => azure.AppServices.FunctionApps;
// TODO: Zero-downtime rotation here with similar slotting?

Просмотреть файл

@ -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<AppSettingConfiguration, IWebApp>
SvgImage = ProviderImages.WEBAPPS_SVG)]
public class AppSettingsWebAppApplicationLifecycleProvider :
SlottableAzureApplicationLifecycleProvider<AppSettingConfiguration, IWebApp>
{
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<IWebApp> 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.";
}
}

Просмотреть файл

@ -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<ConnectionStringConfiguration, IWebApp>
SvgImage = ProviderImages.WEBAPPS_SVG)]
public class ConnectionStringWebAppApplicationLifecycleProvider :
SlottableAzureApplicationLifecycleProvider<ConnectionStringConfiguration, IWebApp>
{
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<IWebApp> 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.";
}
}

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
<PackageReference Include="Azure.Identity" Version="1.1.1" />
</ItemGroup>

Просмотреть файл

@ -5,11 +5,10 @@ using System.Threading.Tasks;
namespace AuthJanitor.Providers.Azure
{
public abstract class AzureApplicationLifecycleProvider<TConfiguration, TResource> : AzureAuthJanitorProvider<TConfiguration, TResource>, IApplicationLifecycleProvider
public abstract class AzureApplicationLifecycleProvider<TConfiguration, TResource> :
AzureAuthJanitorProvider<TConfiguration, TResource>, IApplicationLifecycleProvider
where TConfiguration : AzureAuthJanitorProviderConfiguration
{
public abstract Task AfterRekeying();
public abstract Task BeforeRekeying(List<RegeneratedSecret> temporaryUseSecrets);
public abstract Task CommitNewSecrets(List<RegeneratedSecret> newSecrets);
public abstract Task DistributeLongTermSecretValues(List<RegeneratedSecret> newSecretValues);
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -8,8 +8,6 @@ namespace AuthJanitor.Providers.Azure
public abstract class AzureRekeyableObjectProvider<TConfiguration, TResource> : AzureAuthJanitorProvider<TConfiguration, TResource>, IRekeyableObjectProvider
where TConfiguration : AzureAuthJanitorProviderConfiguration
{
public abstract Task<RegeneratedSecret> GetSecretToUseDuringRekeying();
public abstract Task OnConsumingApplicationSwapped();
public abstract Task<RegeneratedSecret> Rekey(TimeSpan requestedValidPeriod);
}
}

Просмотреть файл

@ -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<TConfiguration, TResource> : AzureApplicationLifecycleProvider<TConfiguration, TResource>
public abstract class SlottableAzureApplicationLifecycleProvider<TConfiguration, TResource> :
AzureApplicationLifecycleProvider<TConfiguration, TResource>,
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<RegeneratedSecret> 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<RegeneratedSecret> 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<RegeneratedSecret> newSecrets)
public override async Task DistributeLongTermSecretValues(List<RegeneratedSecret> 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<RegeneratedSecret> 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<RegeneratedSecret> secrets)
{
var resource = await GetResourceAsync();
await ApplyUpdate(resource, Configuration.TemporarySlot, secrets);
_logger.LogInformation("Swapping to '{SlotName}'", Configuration.TemporarySlot);
await SwapSlotAsync(resource, Configuration.TemporarySlot);
}
}
}

Просмотреть файл

@ -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;
/// <summary>
/// Source Slot (original application)
@ -25,10 +24,10 @@ namespace AuthJanitor.Providers.Azure.Workflows
public string TemporarySlot { get; set; } = DEFAULT_TEMPORARY_SLOT;
/// <summary>
/// Destination Slot (updated application). By default this is the same as the Source Slot.
/// Destination Slot (where the app ends up)
/// </summary>
[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;
}
}

Просмотреть файл

@ -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<TConfiguration, TResource, TKeyring, TKeyType, TSdkKeyType> : AzureRekeyableObjectProvider<TConfiguration, TResource>
public abstract class TwoKeyAzureRekeyableObjectProvider<TConfiguration, TResource, TKeyring, TKeyType, TSdkKeyType> :
AzureRekeyableObjectProvider<TConfiguration, TResource>,
ICanRunSanityTests,
ICanGenerateTemporarySecretValue,
ICanCleanup
where TConfiguration : TwoKeyAzureAuthJanitorProviderConfiguration<TKeyType>
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<RegeneratedSecret> GetSecretToUseDuringRekeying()
public async Task<RegeneratedSecret> 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)
{

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<AccessTokenConfiguration>
SvgImage = ProviderImages.AZURE_AD_SVG)]
public class AccessTokenRekeyableObjectProvider :
RekeyableObjectProvider<AccessTokenConfiguration>
{
private readonly ILogger _logger;

Просмотреть файл

@ -6,8 +6,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.2.1" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Azure.Core" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Maps" Version="1.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.6" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<AzureMapsConfiguration>
SvgImage = ProviderImages.AZURE_MAPS_SVG)]
public class AzureMapsRekeyableObjectProvider :
RekeyableObjectProvider<AzureMapsConfiguration>,
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<RegeneratedSecret> GetSecretToUseDuringRekeying()
public async Task<RegeneratedSecret> 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)
{

Просмотреть файл

@ -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<AzureSearchAdminKeyConfiguration, ISearchService, IAdminKeys, AzureSearchAdminKeyConfiguration.AzureSearchKeyKinds, AdminKeyKind>
{
public AzureSearchAdminKeyRekeyableObjectProvider(ILogger<AzureSearchAdminKeyRekeyableObjectProvider> logger) : base(logger) { }

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Sql.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Sql.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<AzureSqlAdministratorPasswordConfiguration, ISqlServer>
SvgImage = ProviderImages.SQL_SERVER_SVG)]
public class AzureSqlAdministratorPasswordRekeyableObjectProvider :
AzureRekeyableObjectProvider<AzureSqlAdministratorPasswordConfiguration, ISqlServer>,
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<RegeneratedSecret> GetSecretToUseDuringRekeying() => Task.FromResult<RegeneratedSecret>(null);
public override async Task<RegeneratedSecret> 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<RiskyConfigurationItem> GetRisks()
{
List<RiskyConfigurationItem> issues = new List<RiskyConfigurationItem>();

Просмотреть файл

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<CosmosDbKeyConfiguration, ICosmosDBAccount, IDatabaseAccountListKeysResult, CosmosDbKeyConfiguration.CosmosDbKeyKinds, string>
{
private const string PRIMARY_READONLY_KEY = "primaryReadOnly";

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.EventHub.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.EventHub.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<EventHubKeyConfiguration, IEventHubNamespace, IEventHubAuthorizationKey, EventHubKeyConfiguration.EventHubKeyTypes, KeyType>
{
public EventHubRekeyableObjectProvider(ILogger<EventHubRekeyableObjectProvider> logger) : base(logger) { }

Просмотреть файл

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.0.3" />
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.0.4" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.0.3" />
</ItemGroup>

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<KeyVaultKeyConfiguration>
SvgImage = ProviderImages.KEY_VAULT_SVG)]
public class KeyVaultKeyRekeyableObjectProvider :
RekeyableObjectProvider<KeyVaultKeyConfiguration>,
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<RegeneratedSecret> GetSecretToUseDuringRekeying()
public async Task<RegeneratedSecret> GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret to use during rekeying based on current KID");
var client = GetKeyClient();

Просмотреть файл

@ -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<KeyVaultSecretLifecycleConfiguration>
SvgImage = ProviderImages.KEY_VAULT_SVG)]
public class KeyVaultSecretApplicationLifecycleProvider :
ApplicationLifecycleProvider<KeyVaultSecretLifecycleConfiguration>,
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");
}
/// <summary>
/// Call to commit the newly generated secret
/// </summary>
public override async Task CommitNewSecrets(List<RegeneratedSecret> newSecrets)
public override async Task DistributeLongTermSecretValues(List<RegeneratedSecret> 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<KeyVaultSecret> currentSecret = await client.GetSecretAsync(Configuration.SecretName);

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<KeyVaultSecretConfiguration>
SvgImage = ProviderImages.KEY_VAULT_SVG)]
public class KeyVaultSecretRekeyableObjectProvider :
RekeyableObjectProvider<KeyVaultSecretConfiguration>,
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<RegeneratedSecret> GetSecretToUseDuringRekeying()
public async Task<RegeneratedSecret> GenerateTemporarySecretValue()
{
_logger.LogInformation("Getting temporary secret based on current version...");
var client = GetSecretClient();

Просмотреть файл

@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Redis.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Redis.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<RedisCacheKeyConfiguration, IRedisCache, IRedisAccessKeys, RedisCacheKeyConfiguration.RedisKeyTypes, RedisKeyType>
{
protected override string Service => "Redis Cache";

Просмотреть файл

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<ServiceBusKeyConfiguration, IServiceBusNamespace, IAuthorizationKeys, ServiceBusKeyConfiguration.ServiceBusKeyTypes, Policykey>
{
public ServiceBusRekeyableObjectProvider(ILogger<ServiceBusRekeyableObjectProvider> logger) : base(logger) { }

Просмотреть файл

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.33.0" />
<PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.34.0" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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, IStorageAccount, IReadOnlyList<StorageAccountKey>, StorageAccountKeyConfiguration.StorageKeyTypes, string>
{
private const string KEY1 = "key1";
@ -45,7 +42,20 @@ namespace AuthJanitor.Providers.Storage
protected override Task<IReadOnlyList<StorageAccountKey>> RetrieveCurrentKeyring(IStorageAccount resource, string keyType) => resource.GetKeysAsync();
protected override Task<IReadOnlyList<StorageAccountKey>> RotateKeyringValue(IStorageAccount resource, string keyType) => resource.RegenerateKeyAsync(keyType);
protected override async Task<IReadOnlyList<StorageAccountKey>> 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
{

Просмотреть файл

@ -7,11 +7,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.6" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>