Skip to content

Commit

Permalink
#64 Add ability to update and create new secrets (#65) [skip ci]
Browse files Browse the repository at this point in the history
* add ability to create and update secret versions. [skip ci]

add error message if user does not have rights
this closes #64

* chore: cleanup [skip ci]
  • Loading branch information
cricketthomas committed Jul 8, 2024
1 parent 87c4f74 commit 4c4c44a
Show file tree
Hide file tree
Showing 18 changed files with 556 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Desktop/Desktop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@


<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Desktop" Version="11.1.0-rc2" />
</ItemGroup>


Expand Down
34 changes: 34 additions & 0 deletions KeyVaultExplorer/Exceptions/KvExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,38 @@ public KeyVaultItemNotFoundException(string message, Exception inner)
: base(message, inner)
{
}
}

public class KeyVaultItemNotFailedToUpdate : Exception
{
public KeyVaultItemNotFailedToUpdate()
{
}

public KeyVaultItemNotFailedToUpdate(string message)
: base(message)
{
}

public KeyVaultItemNotFailedToUpdate(string message, Exception inner)
: base(message, inner)
{
}
}

public class KeyVaultInSufficientPrivileges : Exception
{
public KeyVaultInSufficientPrivileges()
{
}

public KeyVaultInSufficientPrivileges(string message)
: base(message)
{
}

public KeyVaultInSufficientPrivileges(string message, Exception inner)
: base(message, inner)
{
}
}
20 changes: 13 additions & 7 deletions KeyVaultExplorer/KeyVaultExplorer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,16 @@


<ItemGroup>
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.0-beta2" />
<PackageReference Include="Avalonia.Svg.Skia" Version="11.1.0-beta1" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.0-rc2" />
<PackageReference Include="Avalonia.Svg.Skia" Version="11.1.0-rc1" />
<PackageReference Include="DeviceId" Version="6.6.0" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0-preview5" />
<PackageReference Include="FluentAvaloniaUI" Version="2.1.0-preview6" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0-beta2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0-preview.4.24266.19" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0-preview.4.24267.1" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0-rc2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0-preview.5.24306.7" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0-preview.5.24306.3" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0-preview.4.24266.19" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0-preview.5.24306.7" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.61.3" />
<PackageReference Include="Azure.ResourceManager.KeyVault" Version="1.2.3" />
<PackageReference Include="Azure.Security.KeyVault.Certificates" Version="4.6.0" />
Expand Down Expand Up @@ -93,5 +93,11 @@
</AvaloniaResource>
</ItemGroup>

<ItemGroup>
<Compile Update="Views\Pages\PropertiesDialogs\CreateNewSecretVersion.axaml.cs">
<DependentUpon>CreateNewSecretVersion.axaml</DependentUpon>
</Compile>
</ItemGroup>


</Project>
2 changes: 2 additions & 0 deletions KeyVaultExplorer/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public class AppSettings

[AllowedValues("Inline", "Overlay")]
public string SplitViewDisplayMode { get; set; } = "Inline";
public string PanePlacement { get; set; } = "Left";

}
18 changes: 13 additions & 5 deletions KeyVaultExplorer/Models/KeyVaultValuesAmalgamation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public class KeyVaultContentsAmalgamation

public DateTimeOffset? LastModifiedDate => UpdatedOn.HasValue ? UpdatedOn.Value.ToLocalTime() : CreatedOn!.Value.ToLocalTime();

public string? WhenLastModified => GetRelativeDateString(LastModifiedDate!.Value.UtcDateTime, true);
public string? WhenExpires => ExpiresOn.HasValue ? GetRelativeDateString(ExpiresOn!.Value.UtcDateTime) : "";
public string? WhenLastModified => GetRelativeDateString(LastModifiedDate!.Value, true);
public string? WhenExpires => ExpiresOn.HasValue ? GetRelativeDateString(ExpiresOn!.Value) : "";

public SecretProperties SecretProperties { get; set; } = null!;
public KeyProperties KeyProperties { get; set; } = null!;
Expand All @@ -45,9 +45,16 @@ public class KeyVaultContentsAmalgamation
public string TagValuesString => string.Join(", ", Tags?.Values ?? []);


private string? GetRelativeDateString(DateTime dateTime, bool isPast = false)
private string? GetRelativeDateString(DateTimeOffset dateTimeOffset, bool isPast = false)
{
TimeSpan timeSpan = isPast ? DateTime.Now.Subtract(dateTime) : dateTime.Subtract(DateTime.Now);
DateTimeOffset now = DateTimeOffset.Now;

if (dateTimeOffset < now && !isPast)
{
return "Expired";
}

TimeSpan timeSpan = isPast ? now.Subtract(dateTimeOffset) : dateTimeOffset.Subtract(now);
int dayDifference = (int)timeSpan.TotalDays;
int secondDifference = (int)timeSpan.TotalSeconds;
var weeks = Math.Ceiling((double)dayDifference / 7);
Expand All @@ -68,10 +75,11 @@ public class KeyVaultContentsAmalgamation
(1, _) when !isPast => "tomorrow",
( < 7, _) => $"{(isPast ? "" : "in ")}{dayDifference} days{(isPast ? " ago" : "")}",
( < 30, _) => $"{(isPast ? "" : "in ")}{weeks} {(weeks == 1 ? "week" : "weeks")}{(isPast ? " ago" : "")}",
( < 366, _) => $"{(isPast ? "" : "in ")}{months} {(months == 1 ? "month" : "months")}{(isPast ? " ago" : "")}",
( < 366, _) => $"{(isPast ? "" : "in ")}{months} {(months == 1 ? "month" : "months")}{(isPast ? " ago" : "")}",
(_, _) => $"{(isPast ? "" : "in ")}{years} {(years == 1 ? "year" : "years")}{(isPast ? " ago" : "")}"
};
}

}

public enum KeyVaultItemType
Expand Down
5 changes: 4 additions & 1 deletion KeyVaultExplorer/Resources/Resources.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
xmlns:ui="using:FluentAvalonia.UI.Controls">


<SolidColorBrush x:Key="DynamicActiveBackgroundFAColor" Color="{DynamicResource SolidBackgroundFillColorBase}" />



<SolidColorBrush x:Key="DynamicActiveBackgroundFAColor" Color="{DynamicResource SolidBackgroundFillColorBase}" />
<!--<StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey="SelectedTabViewColorKv" />
<SolidColorBrush x:Key="SelectedTabViewColorKv" Color="Transparent" />-->

Expand Down
9 changes: 8 additions & 1 deletion KeyVaultExplorer/Resources/Styles.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives">
<Design.PreviewWith />

<Style Selector="TextBox">
<Style Selector="TextBox.IsSmall">
<Setter Property="MinHeight" Value="30" />
<Setter Property="MaxHeight" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="8,2,2,2" />
</Style>

<Style Selector="TextBox.isCompact">
<Setter Property="MinHeight" Value="25" />
<Setter Property="Padding" Value="5,0,0,0" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeSmall}" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>

<Style Selector="ui|CommandBarFlyoutCommandBar /template/ Border#LayoutRoot">
<Setter Property="Margin" Value="10" />
<Setter Property="BoxShadow" Value="0 4 10 0 #34000000" />
Expand Down
27 changes: 25 additions & 2 deletions KeyVaultExplorer/Services/VaultService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public async IAsyncEnumerable<KeyVaultResource> GetKeyVaultResources()
}
}
}

public async IAsyncEnumerable<KeyVaultResource> GetKeyVaultsByResourceGroup(ResourceGroupResource resource)
{
var armClient = new ArmClient(new CustomTokenCredential(await _authService.GetAzureArmTokenSilent()));
Expand Down Expand Up @@ -230,12 +231,16 @@ public async Task<KeyVaultSecret> GetSecret(Uri kvUri, string secretName)
{
throw new KeyVaultItemNotFoundException(ex.Message, ex);
}
catch (Exception ex) when (ex.Message.Contains("403"))
{
throw new KeyVaultInSufficientPrivileges(ex.Message, ex);
}
}

public async Task<List<SecretProperties>> GetSecretProperties(Uri kvUri, string name)
public async Task<List<SecretProperties>> GetSecretProperties(Uri keyVaultUri, string name)
{
var token = new CustomTokenCredential(await _authService.GetAzureKeyVaultTokenSilent());
var client = new SecretClient(kvUri, token);
var client = new SecretClient(keyVaultUri, token);
List<SecretProperties> list = new();
try
{
Expand Down Expand Up @@ -269,6 +274,7 @@ public async Task<Dictionary<string, KeyVaultResource>> GetStoredSelectedSubscri
}

public record SubscriptionResourceWithNextPageToken(SubscriptionResource SubscriptionResource, string ContinuationToken);

public async IAsyncEnumerable<CertificateProperties> GetVaultAssociatedCertificates(Uri kvUri)
{
var token = new CustomTokenCredential(await _authService.GetAzureKeyVaultTokenSilent());
Expand Down Expand Up @@ -309,4 +315,21 @@ public async IAsyncEnumerable<KeyVaultResource> GetWithKeyVaultsBySubscriptionAs
yield return kvResource;
}
}

public async Task<KeyVaultSecret> CreateSecret(KeyVaultSecret keyVaultSecret, Uri KeyVaultUri)
{
var token = new CustomTokenCredential(await _authService.GetAzureKeyVaultTokenSilent());
SecretClient client = new SecretClient(KeyVaultUri, token);
return await client.SetSecretAsync(keyVaultSecret);

}

public async Task<SecretProperties> UpdateSecret(SecretProperties secretProperties, Uri KeyVaultUri)
{
var token = new CustomTokenCredential(await _authService.GetAzureKeyVaultTokenSilent());

SecretClient client = new SecretClient(KeyVaultUri, token);

return await client.UpdateSecretPropertiesAsync(secretProperties);
}
}
90 changes: 90 additions & 0 deletions KeyVaultExplorer/ViewModels/CreateNewSecretVersionViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using KeyVaultExplorer.Views;
using KeyVaultExplorer.Services;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
using Azure.Security.KeyVault.Secrets;
using System;

namespace KeyVaultExplorer.ViewModels;

public partial class CreateNewSecretVersionViewModel : ViewModelBase
{
[ObservableProperty]
private bool isBusy = false;

[ObservableProperty]
private bool isEdit = false;

public bool HasActivationDate => KeyVaultSecretModel is not null && KeyVaultSecretModel.NotBefore.HasValue;
public bool HasExpirationDate => KeyVaultSecretModel is not null && KeyVaultSecretModel.ExpiresOn.HasValue;

[ObservableProperty]
private string secretValue;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Location))]
[NotifyPropertyChangedFor(nameof(HasActivationDate))]
[NotifyPropertyChangedFor(nameof(HasExpirationDate))]
private SecretProperties keyVaultSecretModel;

[ObservableProperty]
private TimeSpan? expiresOnTimespan;

[ObservableProperty]
private TimeSpan? notBeforeTimespan;

public string? Location => KeyVaultSecretModel?.VaultUri.ToString();
public string? Identifier => KeyVaultSecretModel?.Id.ToString();

private readonly AuthService _authService;
private readonly VaultService _vaultService;
private NotificationViewModel _notificationViewModel;

public CreateNewSecretVersionViewModel()
{
_authService = Defaults.Locator.GetRequiredService<AuthService>();
_vaultService = Defaults.Locator.GetRequiredService<VaultService>();
_notificationViewModel = Defaults.Locator.GetRequiredService<NotificationViewModel>();
}

[RelayCommand]
public async Task EditDetails()
{
if (KeyVaultSecretModel.NotBefore.HasValue)
KeyVaultSecretModel.NotBefore = KeyVaultSecretModel.NotBefore.Value.Date + (NotBeforeTimespan.HasValue ? NotBeforeTimespan.Value : TimeSpan.Zero);

if (KeyVaultSecretModel.ExpiresOn.HasValue)
KeyVaultSecretModel.ExpiresOn = KeyVaultSecretModel.ExpiresOn.Value.Date + (ExpiresOnTimespan.HasValue ? ExpiresOnTimespan.Value : TimeSpan.Zero);

var updatedProps = await _vaultService.UpdateSecret(KeyVaultSecretModel, KeyVaultSecretModel.VaultUri);
KeyVaultSecretModel = updatedProps;
}

[RelayCommand]
public async Task NewVersion()
{
var newSecret = new KeyVaultSecret(KeyVaultSecretModel.Name, SecretValue);
if (KeyVaultSecretModel.NotBefore.HasValue)
newSecret.Properties.NotBefore = KeyVaultSecretModel.NotBefore.Value.Date + (NotBeforeTimespan.HasValue ? NotBeforeTimespan.Value : TimeSpan.Zero);

if (KeyVaultSecretModel.ExpiresOn.HasValue)
newSecret.Properties.ExpiresOn = KeyVaultSecretModel.ExpiresOn.Value.Date + (ExpiresOnTimespan.HasValue ? ExpiresOnTimespan.Value : TimeSpan.Zero);

newSecret.Properties.ContentType = KeyVaultSecretModel.ContentType;

var newVersion = await _vaultService.CreateSecret(newSecret, KeyVaultSecretModel.VaultUri);
var properties = (await _vaultService.GetSecretProperties(newVersion.Properties.VaultUri, newVersion.Name)).First();
KeyVaultSecretModel = properties;
}

partial void OnKeyVaultSecretModelChanging(SecretProperties model)
{
ExpiresOnTimespan = model is not null && model.ExpiresOn.HasValue ? model?.ExpiresOn.Value.LocalDateTime.TimeOfDay : null;
NotBeforeTimespan = model is not null && model.NotBefore.HasValue ? model?.NotBefore.Value.LocalDateTime.TimeOfDay : null;
}
}
23 changes: 12 additions & 11 deletions KeyVaultExplorer/ViewModels/NotificationViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using FluentAvalonia.UI.Controls;
using System;
using FluentAvalonia.UI.Windowing;
using System.Linq;

namespace KeyVaultExplorer.ViewModels;

Expand All @@ -16,21 +19,19 @@ public void AddMessage(Notification notification)

public async void ShowErrorPopup(Notification notification)
{
// Declaring a TaskDialog from C#:
var td = new TaskDialog
{
// Title property only applies on Windowed dialogs
Title = notification.Title,
Header = notification.Title,
Content = notification.Message,
//IconSource = new SymbolIconSource { Symbol = Symbol.clos },
FooterVisibility = TaskDialogFooterVisibility.Auto,
//Footer = new CheckBox { Content = "Never show me this again" },
FooterVisibility = TaskDialogFooterVisibility.Always,
IsFooterExpanded = false,
Buttons = { TaskDialogButton.CloseButton },

Buttons = { TaskDialogButton.CloseButton }
};
td.XamlRoot = App.Current.GetTopLevel();

var result = await td.ShowAsync();
//Avalonia.Application.Current.GetTopLevel() as AppWindow;
var lifetime = App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
td.XamlRoot = lifetime.Windows.Last() as AppWindow;
await td.ShowAsync(true);
}
}
Loading

0 comments on commit 4c4c44a

Please sign in to comment.