sources/Google.Solutions.IapDesktop.Extensions.Session/ToolWindows/SshKeys/AuthorizedPublicKeysViewModel.cs (202 lines of code) (raw):

// // Copyright 2022 Google LLC // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. // using Google.Solutions.Apis.Compute; using Google.Solutions.Apis.Crm; using Google.Solutions.Common.Diagnostics; using Google.Solutions.IapDesktop.Application; using Google.Solutions.IapDesktop.Application.Windows; using Google.Solutions.IapDesktop.Application.Windows.Dialog; using Google.Solutions.IapDesktop.Core.ObjectModel; using Google.Solutions.IapDesktop.Core.ProjectModel; using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Ssh; using Google.Solutions.Mvvm.Binding; using Google.Solutions.Mvvm.Binding.Commands; using Google.Solutions.Mvvm.Cache; using System; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace Google.Solutions.IapDesktop.Extensions.Session.ToolWindows.SshKeys { [Service] public class AuthorizedPublicKeysViewModel : ModelCachingViewModelBase<IProjectModelNode, AuthorizedPublicKeysModel> { private const int ModelCacheCapacity = 5; private const string WindowTitlePrefix = "Authorized SSH keys"; private readonly IConfirmationDialog confirmationDialog; private readonly IJobService jobService; private readonly Service<IOsLoginProfile> osLoginService; private readonly Service<IComputeEngineClient> computeClient; private readonly Service<IResourceManagerClient> resourceManagerAdapter; private string? filter; private AuthorizedPublicKeysModel.Item? selectedItem; public AuthorizedPublicKeysViewModel( IServiceProvider serviceProvider) : base(ModelCacheCapacity) { this.confirmationDialog = serviceProvider.GetService<IConfirmationDialog>(); this.jobService = serviceProvider.GetService<IJobService>(); this.osLoginService = serviceProvider.GetService<Service<IOsLoginProfile>>(); this.computeClient = serviceProvider.GetService<Service<IComputeEngineClient>>(); this.resourceManagerAdapter = serviceProvider.GetService<Service<IResourceManagerClient>>(); this.IsListEnabled = ObservableProperty.Build(false); this.IsLoading = ObservableProperty.Build(false); this.WindowTitle = ObservableProperty.Build<string?>(null); this.InformationText = ObservableProperty.Build<string?>(null); this.RefreshCommand = ObservableCommand.Build( "Refresh", InvalidateAsync); this.DeleteSelectedItemCommand = ObservableCommand.Build( "Delete", DeleteSelectedItemAsync); } public void ResetWindowTitleAndInformationBar() { this.WindowTitle.Value = WindowTitlePrefix; this.InformationText.Value = null; } //--------------------------------------------------------------------- // Observable "output" properties. //--------------------------------------------------------------------- public RangeObservableCollection<AuthorizedPublicKeysModel.Item> AllKeys { get; } = new RangeObservableCollection<AuthorizedPublicKeysModel.Item>(); public RangeObservableCollection<AuthorizedPublicKeysModel.Item> FilteredKeys { get; } = new RangeObservableCollection<AuthorizedPublicKeysModel.Item>(); public ObservableProperty<bool> IsListEnabled { get; } public ObservableProperty<bool> IsLoading { get; } public ObservableProperty<string?> WindowTitle { get; } public ObservableProperty<string?> InformationText { get; } public AuthorizedPublicKeysModel.Item? SelectedItem { get => this.selectedItem; set { this.selectedItem = value; RaisePropertyChange(); RaisePropertyChange((AuthorizedPublicKeysViewModel m) => m.IsDeleteButtonEnabled); } } public bool IsDeleteButtonEnabled => this.selectedItem != null; //--------------------------------------------------------------------- // "Input" properties. //--------------------------------------------------------------------- public string? Filter { get => this.filter; set { this.filter = value; var matches = this.AllKeys .Where(k => string.IsNullOrEmpty(this.filter) || (k.Key.Email != null && k.Key.Email.Contains(this.filter)) || k.Key.KeyType.Contains(this.filter)); this.FilteredKeys.Clear(); this.FilteredKeys.AddRange(matches); RaisePropertyChange((AuthorizedPublicKeysViewModel m) => m.FilteredKeys); RaisePropertyChange(); } } //--------------------------------------------------------------------- // Commands. //--------------------------------------------------------------------- internal IObservableCommand RefreshCommand { get; } internal IObservableCommand DeleteSelectedItemCommand { get; } //--------------------------------------------------------------------- // Actions. //--------------------------------------------------------------------- private async Task DeleteSelectedItemAsync(CancellationToken cancellationToken) { // // Capture current item, the selection might change any time. // var item = this.selectedItem; if (item == null) { return; } Debug.Assert(this.IsDeleteButtonEnabled); Debug.Assert(this.View != null); var question = "Are you sure you want to delete this key?"; if (item.AuthorizationMethod == KeyAuthorizationMethods.ProjectMetadata) { question += " This change affects all VM instances in the project."; } if (this.confirmationDialog.Confirm( this.View, question, "Delete key for user " + item.Key.Email, "Delete key") != DialogResult.Yes) { return; } await this.jobService.RunAsync( new JobDescription( $"Deleting SSH keys for {item.Key.Email}", JobUserFeedbackType.BackgroundFeedback), async jobToken => { if (item.AuthorizationMethod == KeyAuthorizationMethods.Oslogin) { await AuthorizedPublicKeysModel.DeleteFromOsLoginAsync( this.osLoginService.Activate(), item, cancellationToken) .ConfigureAwait(true); } else { await AuthorizedPublicKeysModel.DeleteFromMetadataAsync( this.computeClient.Activate(), this.resourceManagerAdapter.Activate(), this.ModelKey!, item, cancellationToken) .ConfigureAwait(true); } }).ConfigureAwait(true); // Back to original (UI) thread. // // Refresh list. // await InvalidateAsync() .ConfigureAwait(true); } //--------------------------------------------------------------------- // ModelCachingViewModelBase. //--------------------------------------------------------------------- protected override async Task<AuthorizedPublicKeysModel?> LoadModelAsync( IProjectModelNode node, CancellationToken token) { using (ApplicationTraceSource.Log.TraceMethod().WithParameters(node)) { try { this.IsLoading.Value = true; // // Reset window title, otherwise the default or previous title // stays while data is loading. // ResetWindowTitleAndInformationBar(); // // Load data using a job so that the task is retried in case // of authentication issues. // return await this.jobService.RunAsync( new JobDescription( $"Loading SSH keys for {node.DisplayName}", JobUserFeedbackType.BackgroundFeedback), async jobToken => { return await AuthorizedPublicKeysModel .LoadAsync( this.computeClient.Activate(), this.resourceManagerAdapter.Activate(), this.osLoginService.Activate(), node, jobToken) .ConfigureAwait(false); }).ConfigureAwait(true); // Back to original (UI) thread. } finally { this.IsLoading.Value = false; } } } protected override void ApplyModel(bool cached) { this.AllKeys.Clear(); this.FilteredKeys.Clear(); this.SelectedItem = null; if (this.Model == null) { // // Unsupported node. // this.IsListEnabled.Value = false; this.InformationText.Value = null; this.WindowTitle.Value = WindowTitlePrefix; } else { this.IsListEnabled.Value = true; this.InformationText.Value = this.Model.Warnings.Any() ? string.Join(", ", this.Model.Warnings) : null; this.WindowTitle.Value = WindowTitlePrefix + $": {this.Model.DisplayName}"; this.AllKeys.AddRange(this.Model.Items); } // Reset filter, implicitly populating the FilteredPackages property. this.Filter = string.Empty; } } }