diff --git a/KeyVaultExplorer/Services/VaultService.cs b/KeyVaultExplorer/Services/VaultService.cs index af146bc..0f6857f 100644 --- a/KeyVaultExplorer/Services/VaultService.cs +++ b/KeyVaultExplorer/Services/VaultService.cs @@ -357,4 +357,12 @@ public async Task UpdateSecret(SecretProperties properties, Ur var client = new SecretClient(KeyVaultUri, token); return await client.UpdateSecretPropertiesAsync(properties); } + + public async Task UpdateCertificate(CertificateProperties properties, Uri KeyVaultUri) + { + var token = new CustomTokenCredential(await _authService.GetAzureKeyVaultTokenSilent()); + var client = new CertificateClient(KeyVaultUri, token); + var response = await client.UpdateCertificatePropertiesAsync(properties); + return response.Value.Properties; + } } \ No newline at end of file diff --git a/KeyVaultExplorer/ViewModels/CreateNewSecretVersionViewModel.cs b/KeyVaultExplorer/ViewModels/CreateNewSecretVersionViewModel.cs index e6bdd7a..194314d 100644 --- a/KeyVaultExplorer/ViewModels/CreateNewSecretVersionViewModel.cs +++ b/KeyVaultExplorer/ViewModels/CreateNewSecretVersionViewModel.cs @@ -4,6 +4,7 @@ using KeyVaultExplorer.Services; using KeyVaultExplorer.Validations; using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; @@ -64,6 +65,9 @@ public CreateNewSecretVersionViewModel() ValidateAllProperties(); } + // Delegate to get updated tags from the UI + public Func>? GetUpdatedTags { get; set; } + [ObservableProperty] public bool hasActivationDateChecked; @@ -86,6 +90,9 @@ public async Task EditDetails() else KeyVaultSecretModel.ExpiresOn = null; + // Update tags from the TagsEditor if available + UpdateTagsFromEditor(); + var updatedProps = await _vaultService.UpdateSecret(KeyVaultSecretModel, KeyVaultSecretModel.VaultUri); KeyVaultSecretModel = updatedProps; } @@ -102,6 +109,9 @@ private async Task NewVersion() newSecret.Properties.ContentType = KeyVaultSecretModel.ContentType; + // Update tags from the TagsEditor before creating new version + UpdateTagsFromEditor(); + foreach (var tag in KeyVaultSecretModel.Tags) newSecret.Properties.Tags.Add(tag.Key, tag.Value); @@ -137,6 +147,19 @@ partial void OnHasExpirationDateCheckedChanged(bool oldValue, bool newValue) } } + private void UpdateTagsFromEditor() + { + if (GetUpdatedTags != null) + { + var updatedTags = GetUpdatedTags(); + KeyVaultSecretModel.Tags.Clear(); + foreach (var tag in updatedTags) + { + KeyVaultSecretModel.Tags[tag.Key] = tag.Value; + } + } + } + //public async Task> GetAvailableSubscriptions() //{ // var subscriptions = new List(); diff --git a/KeyVaultExplorer/ViewModels/EditCertificateVersionViewModel.cs b/KeyVaultExplorer/ViewModels/EditCertificateVersionViewModel.cs new file mode 100644 index 0000000..4871cd7 --- /dev/null +++ b/KeyVaultExplorer/ViewModels/EditCertificateVersionViewModel.cs @@ -0,0 +1,96 @@ +using Azure.Security.KeyVault.Certificates; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using KeyVaultExplorer.Services; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KeyVaultExplorer.ViewModels; + +public partial class EditCertificateVersionViewModel : ViewModelBase +{ + private readonly AuthService _authService; + private readonly VaultService _vaultService; + private readonly NotificationViewModel _notificationViewModel; + + [ObservableProperty] + private TimeSpan? expiresOnTimespan; + + [ObservableProperty] + private bool isBusy = false; + + [ObservableProperty] + private CertificateProperties keyVaultCertificateModel = new CertificateProperties(""); + + [ObservableProperty] + private TimeSpan? notBeforeTimespan; + + [ObservableProperty] + public bool hasActivationDateChecked; + + [ObservableProperty] + public bool hasExpirationDateChecked; + + public EditCertificateVersionViewModel() + { + _authService = Defaults.Locator.GetRequiredService(); + _vaultService = Defaults.Locator.GetRequiredService(); + _notificationViewModel = Defaults.Locator.GetRequiredService(); + } + + // Delegate to get updated tags from the UI + public Func>? GetUpdatedTags { get; set; } + + public string? Identifier => KeyVaultCertificateModel?.Id?.ToString(); + public string? Location => KeyVaultCertificateModel?.VaultUri.ToString(); + + [RelayCommand] + public async Task EditDetails() + { + // For certificates, we can only update tags and enabled status + // Date properties are typically managed by the certificate lifecycle + + // Update tags from the TagsEditor if available + UpdateTagsFromEditor(); + + var updatedCert = await _vaultService.UpdateCertificate(KeyVaultCertificateModel, KeyVaultCertificateModel.VaultUri); + KeyVaultCertificateModel = updatedCert; + } + + private void UpdateTagsFromEditor() + { + if (GetUpdatedTags != null) + { + var updatedTags = GetUpdatedTags(); + KeyVaultCertificateModel.Tags.Clear(); + foreach (var tag in updatedTags) + { + KeyVaultCertificateModel.Tags[tag.Key] = tag.Value; + } + } + } + + partial void OnKeyVaultCertificateModelChanging(CertificateProperties value) + { + if (value != null) + { + HasActivationDateChecked = value.NotBefore.HasValue; + HasExpirationDateChecked = value.ExpiresOn.HasValue; + ExpiresOnTimespan = value.ExpiresOn.HasValue ? value.ExpiresOn.Value.LocalDateTime.TimeOfDay : null; + NotBeforeTimespan = value.NotBefore.HasValue ? value.NotBefore.Value.LocalDateTime.TimeOfDay : null; + } + } + + partial void OnHasActivationDateCheckedChanged(bool oldValue, bool newValue) + { + // Certificate dates are usually managed automatically + // This might not be applicable for certificates + } + + partial void OnHasExpirationDateCheckedChanged(bool oldValue, bool newValue) + { + // Certificate dates are usually managed automatically + // This might not be applicable for certificates + } +} \ No newline at end of file diff --git a/KeyVaultExplorer/ViewModels/EditKeyVersionViewModel.cs b/KeyVaultExplorer/ViewModels/EditKeyVersionViewModel.cs new file mode 100644 index 0000000..4fb0abd --- /dev/null +++ b/KeyVaultExplorer/ViewModels/EditKeyVersionViewModel.cs @@ -0,0 +1,107 @@ +using Azure.Security.KeyVault.Keys; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using KeyVaultExplorer.Services; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace KeyVaultExplorer.ViewModels; + +public partial class EditKeyVersionViewModel : ViewModelBase +{ + private readonly AuthService _authService; + private readonly VaultService _vaultService; + private readonly NotificationViewModel _notificationViewModel; + + [ObservableProperty] + private TimeSpan? expiresOnTimespan; + + [ObservableProperty] + private bool isBusy = false; + + [ObservableProperty] + private KeyProperties keyVaultKeyModel = new KeyProperties(""); + + [ObservableProperty] + private TimeSpan? notBeforeTimespan; + + [ObservableProperty] + public bool hasActivationDateChecked; + + [ObservableProperty] + public bool hasExpirationDateChecked; + + public EditKeyVersionViewModel() + { + _authService = Defaults.Locator.GetRequiredService(); + _vaultService = Defaults.Locator.GetRequiredService(); + _notificationViewModel = Defaults.Locator.GetRequiredService(); + } + + // Delegate to get updated tags from the UI + public Func>? GetUpdatedTags { get; set; } + + public string? Identifier => KeyVaultKeyModel?.Id?.ToString(); + public string? Location => KeyVaultKeyModel?.VaultUri.ToString(); + + [RelayCommand] + public async Task EditDetails() + { + if (KeyVaultKeyModel.NotBefore.HasValue && HasActivationDateChecked) + KeyVaultKeyModel.NotBefore = KeyVaultKeyModel.NotBefore.Value.Date + (NotBeforeTimespan ?? TimeSpan.Zero); + else + KeyVaultKeyModel.NotBefore = null; + + if (KeyVaultKeyModel.ExpiresOn.HasValue && HasExpirationDateChecked) + KeyVaultKeyModel.ExpiresOn = KeyVaultKeyModel.ExpiresOn.Value.Date + (ExpiresOnTimespan ?? TimeSpan.Zero); + else + KeyVaultKeyModel.ExpiresOn = null; + + // Update tags from the TagsEditor if available + UpdateTagsFromEditor(); + + var updatedKey = await _vaultService.UpdateKey(KeyVaultKeyModel, KeyVaultKeyModel.VaultUri); + KeyVaultKeyModel = updatedKey.Properties; + } + + private void UpdateTagsFromEditor() + { + if (GetUpdatedTags != null) + { + var updatedTags = GetUpdatedTags(); + KeyVaultKeyModel.Tags.Clear(); + foreach (var tag in updatedTags) + { + KeyVaultKeyModel.Tags[tag.Key] = tag.Value; + } + } + } + + partial void OnKeyVaultKeyModelChanging(KeyProperties value) + { + if (value != null) + { + HasActivationDateChecked = value.NotBefore.HasValue; + HasExpirationDateChecked = value.ExpiresOn.HasValue; + ExpiresOnTimespan = value.ExpiresOn.HasValue ? value.ExpiresOn.Value.LocalDateTime.TimeOfDay : null; + NotBeforeTimespan = value.NotBefore.HasValue ? value.NotBefore.Value.LocalDateTime.TimeOfDay : null; + } + } + + partial void OnHasActivationDateCheckedChanged(bool oldValue, bool newValue) + { + if (newValue is false) + { + KeyVaultKeyModel.NotBefore = null; + } + } + + partial void OnHasExpirationDateCheckedChanged(bool oldValue, bool newValue) + { + if (newValue is false) + { + KeyVaultKeyModel.ExpiresOn = null; + } + } +} \ No newline at end of file diff --git a/KeyVaultExplorer/ViewModels/PropertiesPageViewModel.cs b/KeyVaultExplorer/ViewModels/PropertiesPageViewModel.cs index 070c740..a3002c3 100644 --- a/KeyVaultExplorer/ViewModels/PropertiesPageViewModel.cs +++ b/KeyVaultExplorer/ViewModels/PropertiesPageViewModel.cs @@ -238,6 +238,72 @@ private async Task EditVersion() DataContext = viewModel }; } + else if (IsKey) + { + var currentItem = KeyPropertiesList.OrderByDescending(x => x.CreatedOn).First(); + var viewModel = new EditKeyVersionViewModel(); + + viewModel.KeyVaultKeyModel = currentItem; + dialog.PrimaryButtonClick += async (sender, args) => + { + var def = args.GetDeferral(); + try + { + await viewModel.EditDetailsCommand.ExecuteAsync(null); + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification("Success", "The key properties have been updated.")); + } + catch (KeyVaultInsufficientPrivilegesException ex) + { + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification { Message = ex.Message, Title = "Insufficient Privileges" }); + } + catch (Exception ex) + { + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification { Message = ex.Message, Title = "Error" }); + } + finally + { + def.Complete(); + } + }; + + dialog.Content = new EditKeyVersion() + { + DataContext = viewModel + }; + } + else if (IsCertificate) + { + var currentItem = CertificatePropertiesList.OrderByDescending(x => x.CreatedOn).First(); + var viewModel = new EditCertificateVersionViewModel(); + + viewModel.KeyVaultCertificateModel = currentItem; + dialog.PrimaryButtonClick += async (sender, args) => + { + var def = args.GetDeferral(); + try + { + await viewModel.EditDetailsCommand.ExecuteAsync(null); + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification("Success", "The certificate properties have been updated.")); + } + catch (KeyVaultInsufficientPrivilegesException ex) + { + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification { Message = ex.Message, Title = "Insufficient Privileges" }); + } + catch (Exception ex) + { + _notificationViewModel.ShowPopup(new Avalonia.Controls.Notifications.Notification { Message = ex.Message, Title = "Error" }); + } + finally + { + def.Complete(); + } + }; + + dialog.Content = new EditCertificateVersion() + { + DataContext = viewModel + }; + } var result = await dialog.ShowAsync(); } catch (KeyVaultItemNotFoundException ex) diff --git a/KeyVaultExplorer/Views/CustomControls/TagsEditor.axaml b/KeyVaultExplorer/Views/CustomControls/TagsEditor.axaml new file mode 100644 index 0000000..0a1e1d9 --- /dev/null +++ b/KeyVaultExplorer/Views/CustomControls/TagsEditor.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + +