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