sources/Google.Solutions.IapDesktop.Extensions.Session/Settings/ConnectionSettings.cs (370 lines of code) (raw):
//
// Copyright 2020 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.Locator;
using Google.Solutions.Common.Util;
using Google.Solutions.IapDesktop.Core.ClientModel.Protocol;
using Google.Solutions.IapDesktop.Core.ProjectModel;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Rdp;
using Google.Solutions.IapDesktop.Extensions.Session.Protocol.Ssh;
using Google.Solutions.Settings;
using Google.Solutions.Settings.Collection;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security;
#pragma warning disable CA1027 // Mark enums with FlagsAttribute
namespace Google.Solutions.IapDesktop.Extensions.Session.Settings
{
public class ConnectionSettings : ISettingsCollection
{
/// <summary>
/// Resource (instance, zone, project) that these settings apply to.
/// </summary>
public ComputeEngineLocator Resource { get; }
/// <summary>
/// Create new empty settings.
/// </summary>
/// <param name="resource"></param>
internal ConnectionSettings(ComputeEngineLocator resource)
: this(resource, new DictionarySettingsStore(new Dictionary<string, string>()))
{
}
public ConnectionSettings(
ComputeEngineLocator resource,
ISettingsStore store)
{
this.Resource = resource.ExpectNotNull(nameof(resource));
//
// RDP Settings.
//
this.RdpUsername = store.Read<string?>(
"Username",
"Username",
"Username of a local user, SAM account name of a domain user, or UPN (user@domain).",
Categories.WindowsCredentials,
null,
_ => true);
this.RdpPassword = store.Read<SecureString?>(
"Password",
"Password",
"Windows logon password.",
Categories.WindowsCredentials,
null);
this.RdpDomain = store.Read<string?>(
"Domain",
"Domain",
"NetBIOS domain name or computer name. Leave blank when using UPN as username.",
Categories.WindowsCredentials,
null,
_ => true);
this.RdpConnectionBar = store.Read<RdpConnectionBarState>(
"ConnectionBar",
"Connection bar",
"Show connection bar in full-screen mode.",
Categories.RdpDisplay,
RdpConnectionBarState._Default);
this.RdpAuthenticationLevel = store.Read<RdpAuthenticationLevel>(
"AuthenticationLevel",
null,
null,
null,
Protocol.Rdp.RdpAuthenticationLevel._Default);
this.RdpColorDepth = store.Read<RdpColorDepth>(
"ColorDepth",
"Color depth",
"Color depth of remote desktop.",
Categories.RdpDisplay,
Protocol.Rdp.RdpColorDepth._Default);
this.RdpAudioPlayback = store.Read<RdpAudioPlayback>(
"AudioMode",
"Audio playback",
"Select where to play audio.",
Categories.RdpResources,
Protocol.Rdp.RdpAudioPlayback._Default);
this.RdpAudioInput = store.Read<RdpAudioInput>(
"RdpAudioInput",
"Microphone",
"Share default input device so that you can use it on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpAudioInput._Default);
this.RdpAutomaticLogon = store.Read<RdpAutomaticLogon>(
"RdpUserAuthenticationBehavior",
"Automatic logon",
"Log on automatically using saved credentials if possible. Disable if the VM " +
"is configured to always prompt for passwords upon connection (a server-side " +
"group policy)",
Categories.RdpSecurity,
//
// Adjust default based on local RDP policy. If the local policy
// disables saving, then there's a good chance that VMs might be
// configured to do the same.
//
// We don't treat this as a policy though, so users can change
// the setting.
//
LocalRdpPolicy.IsPasswordSavingDisabled
? Protocol.Rdp.RdpAutomaticLogon.Disabled
: Protocol.Rdp.RdpAutomaticLogon._Default);
this.RdpNetworkLevelAuthentication = store.Read<RdpNetworkLevelAuthentication>(
"NetworkLevelAuthentication",
"Network level authentication",
"Secure connection using network level authentication (NLA). " +
"Disable NLA only if the VM uses a custom credential service provider." +
"Disabling NLA automatically enables server authentication.",
Categories.RdpSecurity,
Protocol.Rdp.RdpNetworkLevelAuthentication._Default);
this.RdpConnectionTimeout = store.Read<int>(
"ConnectionTimeout",
"Connection timeout",
"Timeout for establishing a Remote Desktop connection, in seconds. " +
"Use a timeout that allows sufficient time for credential prompts.",
Categories.RdpConnection,
(int)RdpParameters.DefaultConnectionTimeout.TotalSeconds,
Predicate.InRange(0, 300));
this.RdpPort = store.Read<int>(
"RdpPort",
"Server port",
"Server port.",
Categories.RdpConnection,
RdpParameters.DefaultPort,
Predicate.InRange(1, ushort.MaxValue));
this.RdpTransport = store.Read<SessionTransportType>(
"TransportType",
"Connect via",
$"Type of transport. Use {SessionTransportType.IapTunnel} unless " +
"you need to connect to a VM's internal IP address via " +
"Cloud VPN or Interconnect.",
Categories.RdpConnection,
SessionTransportType._Default);
this.RdpRedirectClipboard = store.Read<RdpRedirectClipboard>(
"RedirectClipboard",
"Clipboard",
"Share clipboard contents between your local computer and the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectClipboard._Default);
this.RdpRedirectPrinter = store.Read<RdpRedirectPrinter>(
"RdpRedirectPrinter",
"Printers",
"Share local printers so that you can use them on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectPrinter._Default);
this.RdpRedirectSmartCard = store.Read<RdpRedirectSmartCard>(
"RdpRedirectSmartCard",
"Smart cards",
"Share smart cards so that you can use them on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectSmartCard._Default);
this.RdpRedirectPort = store.Read<RdpRedirectPort>(
"RdpRedirectPort",
"Local ports",
"Share local ports (COM, LPT) so that you can access them on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectPort._Default);
this.RdpRedirectDrive = store.Read<RdpRedirectDrive>(
"RdpRedirectDrive",
"Drives",
"Share local drives so that you can access them on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectDrive._Default);
this.RdpRedirectDevice = store.Read<RdpRedirectDevice>(
"RdpRedirectDevice",
"Plug and Play devices",
"Share local Plug and Play devices so that you can use them on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectDevice._Default);
this.RdpRedirectWebAuthn = store.Read<RdpRedirectWebAuthn>(
"RdpRedirectWebAuthn",
"WebAuthn authenticators",
"Share WebAuthn authenticators and Windows Hello devices so that you can use WebAuthn on the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpRedirectWebAuthn._Default);
this.RdpHookWindowsKeys = store.Read<RdpHookWindowsKeys>(
"RdpHookWindowsKeys",
"Windows shortcuts",
"Redirect Windows shortcuts (like Win+R) to the remote VM.",
Categories.RdpResources,
Protocol.Rdp.RdpHookWindowsKeys._Default);
this.RdpRestrictedAdminMode = store.Read<RdpRestrictedAdminMode>(
"RdpRestrictedAdminMode",
"Restricted Admin mode",
"Disable the transmission of reusable credentials to the VM. This mode requires " +
"a user account with local administrator privileges on the VM, and the " +
"VM must be configured to permit Restricted Admin mode.",
Categories.RdpSecurity,
Protocol.Rdp.RdpRestrictedAdminMode._Default);
this.RdpSessionType = store.Read<RdpSessionType>(
"RdpSessionType",
"Session type",
"Type of RDP session. Use an Admin session to administer an RDS server without " +
"consuming a CAL, otherwise use a User session.",
Categories.RdpSecurity,
Protocol.Rdp.RdpSessionType._Default);
this.RdpDpiScaling = store.Read<RdpDpiScaling>(
"RdpDpiScaling",
"Display scaling",
"Scale remote display to match local scaling settings.",
Categories.RdpDisplay,
Protocol.Rdp.RdpDpiScaling._Default);
this.RdpDesktopSize = store.Read<RdpDesktopSize>(
"DesktopSize",
"Display resolution",
"Display resolution of remote desktop.",
Categories.RdpDisplay,
Protocol.Rdp.RdpDesktopSize._Default);
//
// SSH Settings.
//
this.SshPort = store.Read<int>(
"SshPort",
"Server port",
"Server port",
Categories.SshConnection,
SshParameters.DefaultPort,
Predicate.InRange(1, ushort.MaxValue));
this.SshTransport = store.Read<SessionTransportType>(
"TransportType",
"Connect via",
$"Type of transport. Use {SessionTransportType.IapTunnel} unless " +
"you need to connect to a VM's internal IP address via " +
"Cloud VPN or Interconnect.",
Categories.SshConnection,
SessionTransportType._Default);
this.SshPublicKeyAuthentication = store.Read<SshPublicKeyAuthentication>(
"SshPublicKeyAuthentication",
"Public key authentication",
"Automatically create an SSH key pair and publish it using OS Login or metadata keys.",
Categories.SshCredentials,
Protocol.Ssh.SshPublicKeyAuthentication._Default);
this.SshUsername = store.Read<string?>(
"SshUsername",
"Username",
"Linux username, optional",
Categories.SshCredentials,
null,
username => username == null ||
string.IsNullOrEmpty(username) ||
LinuxUser.IsValidUsername(username));
this.SshPassword = store.Read<SecureString?>(
"SshPassword",
"Password",
"Password, only applicable if public key authentication is disabled",
Categories.SshCredentials,
null);
this.SshConnectionTimeout = store.Read<int>(
"SshConnectionTimeout",
"Connection timeout",
"Timeout for establishing SSH connections, in seconds.",
Categories.SshConnection,
(int)SshParameters.DefaultConnectionTimeout.TotalSeconds,
Predicate.InRange(0, 300));
//
// App Settings.
//
this.AppUsername = store.Read<string?>(
"AppUsername",
null, // Hidden.
null, // Hidden.
null, // Hidden.
null,
username => string.IsNullOrEmpty(username) || !username.Contains(' '));
this.AppNetworkLevelAuthentication = store.Read<AppNetworkLevelAuthenticationState>(
"AppNetworkLevelAuthentication",
"Windows authentication",
"Use Windows authentication for SQL Server connections.",
Categories.AppCredentials,
AppNetworkLevelAuthenticationState._Default);
Debug.Assert(this.Settings.All(s => s != null));
}
//---------------------------------------------------------------------
// RDP settings.
//---------------------------------------------------------------------
public ISetting<string?> RdpUsername { get; }
public ISetting<SecureString?> RdpPassword { get; }
public ISetting<string?> RdpDomain { get; }
public ISetting<RdpConnectionBarState> RdpConnectionBar { get; }
public ISetting<RdpAuthenticationLevel> RdpAuthenticationLevel { get; }
public ISetting<RdpColorDepth> RdpColorDepth { get; }
public ISetting<RdpAudioPlayback> RdpAudioPlayback { get; }
public ISetting<RdpAudioInput> RdpAudioInput { get; }
public ISetting<RdpAutomaticLogon> RdpAutomaticLogon { get; }
public ISetting<RdpNetworkLevelAuthentication> RdpNetworkLevelAuthentication { get; }
public ISetting<int> RdpConnectionTimeout { get; }
public ISetting<int> RdpPort { get; }
public ISetting<SessionTransportType> RdpTransport { get; }
public ISetting<RdpRedirectClipboard> RdpRedirectClipboard { get; }
public ISetting<RdpRedirectPrinter> RdpRedirectPrinter { get; }
public ISetting<RdpRedirectSmartCard> RdpRedirectSmartCard { get; }
public ISetting<RdpRedirectPort> RdpRedirectPort { get; }
public ISetting<RdpRedirectDrive> RdpRedirectDrive { get; }
public ISetting<RdpRedirectDevice> RdpRedirectDevice { get; }
public ISetting<RdpRedirectWebAuthn> RdpRedirectWebAuthn { get; }
public ISetting<RdpHookWindowsKeys> RdpHookWindowsKeys { get; }
public ISetting<RdpRestrictedAdminMode> RdpRestrictedAdminMode { get; }
public ISetting<RdpSessionType> RdpSessionType { get; }
public ISetting<RdpDpiScaling> RdpDpiScaling { get; }
public ISetting<RdpDesktopSize> RdpDesktopSize { get; private set; }
internal IEnumerable<ISetting> RdpSettings => new ISetting[]
{
//
// NB. The order determines the default order in the PropertyGrid
// (assuming the PropertyGrid doesn't force alphabetical order).
//
this.RdpTransport,
this.RdpConnectionTimeout,
this.RdpPort,
this.RdpUsername,
this.RdpPassword,
this.RdpDomain,
this.RdpColorDepth,
this.RdpConnectionBar,
this.RdpDesktopSize,
this.RdpDpiScaling,
this.RdpAudioPlayback,
this.RdpAudioInput,
this.RdpHookWindowsKeys,
this.RdpRedirectClipboard,
this.RdpRedirectPrinter,
this.RdpRedirectSmartCard,
this.RdpRedirectPort,
this.RdpRedirectDrive,
this.RdpRedirectDevice,
this.RdpRedirectWebAuthn,
this.RdpAutomaticLogon,
this.RdpNetworkLevelAuthentication,
this.RdpAuthenticationLevel,
this.RdpRestrictedAdminMode,
this.RdpSessionType,
};
//---------------------------------------------------------------------
// SSH settings.
//---------------------------------------------------------------------
public ISetting<int> SshPort { get; private set; }
public ISetting<SessionTransportType> SshTransport { get; private set; }
public ISetting<string?> SshUsername { get; private set; }
public ISetting<SecureString?> SshPassword { get; private set; }
public ISetting<int> SshConnectionTimeout { get; private set; }
public ISetting<SshPublicKeyAuthentication> SshPublicKeyAuthentication { get; private set; }
internal IEnumerable<ISetting> SshSettings => new ISetting[]
{
//
// NB. The order determines the default order in the PropertyGrid
// (assuming the PropertyGrid doesn't force alphabetical order).
//
this.SshTransport,
this.SshConnectionTimeout,
this.SshPort,
this.SshPublicKeyAuthentication,
this.SshUsername,
this.SshPassword,
};
//---------------------------------------------------------------------
// App settings.
//---------------------------------------------------------------------
public ISetting<string?> AppUsername { get; private set; }
public ISetting<AppNetworkLevelAuthenticationState> AppNetworkLevelAuthentication { get; private set; }
internal IEnumerable<ISetting> AppSettings => new ISetting[]
{
//
// NB. The order determines the default order in the PropertyGrid
// (assuming the PropertyGrid doesn't force alphabetical order).
//
this.AppUsername,
this.AppNetworkLevelAuthentication
};
//---------------------------------------------------------------------
// Filtering.
//---------------------------------------------------------------------
internal bool AppliesTo(
ISetting setting,
IProjectModelInstanceNode node)
{
if (this.SshSettings.Contains(setting))
{
return node.IsSshSupported();
}
else if (this.RdpSettings.Contains(setting))
{
return node.IsRdpSupported();
}
else
{
return true;
}
}
//---------------------------------------------------------------------
// ISettingsCollection.
//---------------------------------------------------------------------
public IEnumerable<ISetting> Settings
{
get => this.RdpSettings
.Concat(this.SshSettings)
.Concat(this.AppSettings);
}
private static class Categories
{
private const ushort MaxIndex = 7;
private static string Order(ushort order, string name)
{
//
// The PropertyGrid control doesn't let us explicitly specify the
// order of categories. To work around that limitation, prefix
// category names with zero-width spaces so that alphabetical
// sorting yields the desired result.
//
Debug.Assert(order <= MaxIndex);
return new string('\u200B', MaxIndex - order) + name;
}
public static readonly string WindowsCredentials = Order(0, "Windows Credentials");
public static readonly string RdpConnection = Order(1, "Remote Desktop Connection");
public static readonly string RdpDisplay = Order(2, "Remote Desktop Display");
public static readonly string RdpResources = Order(3, "Remote Desktop Resources");
public static readonly string RdpSecurity = Order(4, "Remote Desktop Security Settings");
public static readonly string SshConnection = Order(5, "SSH Connection");
public static readonly string SshCredentials = Order(6, "SSH Credentials");
public static readonly string AppCredentials = Order(7, "SQL Server");
}
}
}