/******************************************************************************* * Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"). You may not use * this file except in compliance with the License. A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. * This file 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. * ***************************************************************************** * * AWS Tools for Windows (TM) PowerShell (TM) * */ using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; using System.Net; using System.Text; using System.Management.Automation.Host; using Amazon.Runtime; using Amazon.Util; using Amazon.Runtime.CredentialManagement; namespace Amazon.PowerShell.Common { #region Credentials arguments public enum CredentialsSource { Strings, Profile, CredentialsObject, Session, Environment, Container, InstanceProfile, Unknown } internal interface IAWSCredentialsArguments { string ProfileName { get; } string ProfileLocation { get; } string AccessKey { get; } string SecretKey { get; } string SessionToken { get; } AWSCredentials Credential { get; } PSCredential NetworkCredential { get; } CredentialProfileOptions GetCredentialProfileOptions(); } internal interface IAWSCredentialsArgumentsFull : IAWSCredentialsArguments { string ExternalID { get; } string MfaSerial { get; } string RoleArn { get; } string SourceProfile { get; } } /// /// Wrapper around a set of AWSCredentials (various leaf types) carrying credential data, /// logical name and source info. $StoredAWSCredentials points to an instance of this and /// the ToString() override allows us to display more useful info (the set name) than /// what AWSCredentials on its own can at present. /// public class AWSPSCredentials { internal AWSCredentials Credentials { get; private set; } internal string Name { get; private set; } internal CredentialsSource Source { get; private set; } public override string ToString() { if (!string.IsNullOrEmpty(this.Name)) return this.Name; else return Credentials != null ? Credentials.ToString() : base.ToString(); } internal AWSPSCredentials(AWSCredentials credentials, string name, CredentialsSource source) { this.Credentials = credentials; this.Name = name; this.Source = source; } private AWSPSCredentials() { } } /// /// Performs a search amongst a chain of credential parameters and provider methods to /// arrive at at set of AWS credentials. /// internal static class ICredentialsArgumentsMethods { public static bool TryGetCredentials(this IAWSCredentialsArguments self, PSHost psHost, out AWSPSCredentials credentials, SessionState sessionState) { if (self == null) throw new ArgumentNullException("self"); credentials = null; AWSCredentials innerCredentials = null; string name = null; var source = CredentialsSource.Unknown; var userSpecifiedProfile = !string.IsNullOrEmpty(self.ProfileName); var profileChain = new CredentialProfileStoreChain(self.ProfileLocation); // we probe for credentials by first checking the bound parameters to see if explicit credentials // were supplied (keys, profile name, credential object), overriding anything in the shell environment if (AWSCredentialsFactory.TryGetAWSCredentials(self.GetCredentialProfileOptions(), profileChain, out innerCredentials)) { source = CredentialsSource.Strings; name = "Supplied Key Parameters"; SetProxyAndCallbackIfNecessary(innerCredentials, self, psHost, sessionState); } // user gave us the profile name? if (innerCredentials == null && userSpecifiedProfile) { CredentialProfile credentialProfile; if (profileChain.TryGetProfile(self.ProfileName, out credentialProfile)) { innerCredentials = AWSCredentialsFactory.GetAWSCredentials(credentialProfile, profileChain); source = CredentialsSource.Profile; name = self.ProfileName; SetProxyAndCallbackIfNecessary(innerCredentials, self, psHost, sessionState); } else { // if the user gave us an explicit profile name (and optional location) it's an error if we // don't find it as otherwise we could drop through and pick up a 'default' profile that is // for a different account return false; } } // how about an aws credentials object? if (innerCredentials == null && self.Credential != null) { innerCredentials = self.Credential; source = CredentialsSource.CredentialsObject; name = "Credentials Object"; // don't set proxy and callback, use self.Credential as-is } // shell session variable set (this allows override of machine-wide environment variables) if (innerCredentials == null && sessionState != null) { var variableValue = sessionState.PSVariable.GetValue(SessionKeys.AWSCredentialsVariableName); if (variableValue is AWSPSCredentials) { credentials = variableValue as AWSPSCredentials; source = CredentialsSource.Session; innerCredentials = credentials.Credentials; // so remaining probes are skipped // don't set proxy and callback, use credentials.Credentials as-is } } // no explicit command-level or shell instance override set, start to inspect the environment // starting environment variables if (innerCredentials == null) { try { var environmentCredentials = new EnvironmentVariablesAWSCredentials(); innerCredentials = environmentCredentials; source = CredentialsSource.Environment; name = "Environment Variables"; // no need to set proxy and callback - only basic or session credentials } catch { } } // get credentials from a 'default' profile? if (innerCredentials == null && !userSpecifiedProfile) { CredentialProfile credentialProfile; if (profileChain.TryGetProfile(SettingsStore.PSDefaultSettingName, out credentialProfile) && credentialProfile.CanCreateAWSCredentials) { innerCredentials = AWSCredentialsFactory.GetAWSCredentials(credentialProfile, profileChain); source = CredentialsSource.Profile; name = SettingsStore.PSDefaultSettingName; SetProxyAndCallbackIfNecessary(innerCredentials, self, psHost, sessionState); } } // get credentials from a legacy default profile name? if (innerCredentials == null) { CredentialProfile credentialProfile; if (profileChain.TryGetProfile(SettingsStore.PSLegacyDefaultSettingName, out credentialProfile) && credentialProfile.CanCreateAWSCredentials) { if (AWSCredentialsFactory.TryGetAWSCredentials(credentialProfile, profileChain, out innerCredentials)) { source = CredentialsSource.Profile; name = SettingsStore.PSLegacyDefaultSettingName; SetProxyAndCallbackIfNecessary(innerCredentials, self, psHost, sessionState); } } } if (innerCredentials == null) { // try and load credentials from ECS endpoint (if the relevant environment variable is set) // or EC2 Instance Profile as a last resort try { string relativeUri = System.Environment.GetEnvironmentVariable(ECSTaskCredentials.ContainerCredentialsURIEnvVariable); string fullUri = System.Environment.GetEnvironmentVariable(ECSTaskCredentials.ContainerCredentialsFullURIEnvVariable); if (!string.IsNullOrEmpty(relativeUri) || !string.IsNullOrEmpty(fullUri)) { innerCredentials = new ECSTaskCredentials(); source = CredentialsSource.Container; name = "Container"; // no need to set proxy and callback } else { innerCredentials = new InstanceProfileAWSCredentials(); source = CredentialsSource.InstanceProfile; name = "Instance Profile"; // no need to set proxy and callback } } catch { innerCredentials = null; } } if (credentials == null && innerCredentials != null) { credentials = new AWSPSCredentials(innerCredentials, name, source); } return (credentials != null); } private static void SetProxyAndCallbackIfNecessary(AWSCredentials innerCredentials, IAWSCredentialsArguments self, PSHost psHost, SessionState sessionState) { SetupIfFederatedCredentials(innerCredentials, psHost, self, sessionState); SetupIfAssumeRoleCredentials(innerCredentials, self, sessionState); } private static WebProxy GetWebProxy(IAWSCredentialsArguments self, SessionState sessionState) { var proxySettings = ProxySettings.GetFromSettingsVariable(sessionState); return proxySettings != null ? proxySettings.GetWebProxy() : null; } private static void SetupIfFederatedCredentials(AWSCredentials credentials, PSHost psHost, IAWSCredentialsArguments self, SessionState sessionState) { // if we have picked up a SAML-based credentials profile, make sure the callback // to authenticate the user is set. The underlying SDK will then call us back // if it needs to (we could skip setting if the profile indicates its for the // default identity, but it's simpler to just set up anyway) var samlCredentials = credentials as FederatedAWSCredentials; if (samlCredentials != null) { // set up callback var state = new SAMLCredentialCallbackState { Host = psHost, CmdletNetworkCredentialParameter = self.NetworkCredential }; samlCredentials.Options.CredentialRequestCallback = UserCredentialCallbackHandler; samlCredentials.Options.CustomCallbackState = state; //set up proxy samlCredentials.Options.ProxySettings = GetWebProxy(self, sessionState); } } private static void SetupIfAssumeRoleCredentials(AWSCredentials credentials, IAWSCredentialsArguments self, SessionState sessionState) { var assumeRoleCredentials = credentials as AssumeRoleAWSCredentials; if (assumeRoleCredentials != null) { // set up callback assumeRoleCredentials.Options.MfaTokenCodeCallback = ReadMFACode; // set up proxy assumeRoleCredentials.Options.ProxySettings = GetWebProxy(self, sessionState); } } private static String ReadMFACode() { Console.Write("Enter MFA code:"); String mfaCode = ""; while (true) { ConsoleKeyInfo info = Console.ReadKey(true); if (info.Key == ConsoleKey.Backspace) { if (mfaCode.Length > 0) { // remove the character from the string mfaCode = mfaCode.Remove(mfaCode.Length - 1); // remove the * from the console var position = Console.CursorLeft - 1; Console.SetCursorPosition(position, Console.CursorTop); Console.Write(" "); Console.SetCursorPosition(position, Console.CursorTop); } } else if (info.Key == ConsoleKey.Enter) { Console.WriteLine(); break; } else { mfaCode += info.KeyChar; Console.Write("*"); } } return mfaCode; } private static NetworkCredential UserCredentialCallbackHandler(CredentialRequestCallbackArgs args) { var callbackContext = args.CustomState as SAMLCredentialCallbackState; if (callbackContext == null) // not our callback, so don't attempt to handle return null; // if we are not retrying due to auth failure, did the user pre-supply a credential // via the -NetworkCredential parameter to either the cmdlet or Set-AWSCredentials? PSCredential psCredential = null; string msgPrompt = null; if (!args.PreviousAuthenticationFailed) { if (callbackContext.CmdletNetworkCredentialParameter != null) { psCredential = callbackContext.CmdletNetworkCredentialParameter; callbackContext.CmdletNetworkCredentialParameter = null; // the cmdlet override is single use } else if (callbackContext.ShellNetworkCredentialParameter != null) psCredential = callbackContext.ShellNetworkCredentialParameter; else msgPrompt = string.Format("Enter your credentials to authenticate and obtain AWS role credentials for the profile '{0}'", args.ProfileName); } else msgPrompt = string.Format("Authentication failed. Enter the password for '{0}' to try again.", args.UserIdentity); // some profiles have a user identity expressed in email terms with a mishandled domain, leading to // identity of \me@domain.com - the user then has to strip the \, so let's do it for them var userIdentity = string.IsNullOrEmpty(args.UserIdentity) ? null : args.UserIdentity.TrimStart('\\'); if (psCredential == null) psCredential = callbackContext.Host.UI.PromptForCredential("Authenticating for AWS Role Credentials", msgPrompt, userIdentity, ""); return psCredential != null ? psCredential.GetNetworkCredential() : null; } } /// /// Captures the PSHost and executing cmdlet state for use in our credential callback /// handler. /// internal class SAMLCredentialCallbackState { /// /// The execution host, used to display credential prompts /// public PSHost Host { get; set; } /// /// Any PSCredential argument supplied to the current cmdlet invocation. /// This overrides ShellNetworkCredentialParameter that may have been set /// in the shell when Set-AWSCredentials was invoked. The value is cleared /// after use. /// public PSCredential CmdletNetworkCredentialParameter { get; set; } /// /// Null or the value of the NetworkCredential parameter that was supplied /// when the role profile was set active in the shell via Set-AWSCredentials. /// If set, this credential is used if a more local scope credential cannot /// be found in SelfNetworkCredentialParameter. This value is retained after /// use. /// public PSCredential ShellNetworkCredentialParameter { get; set; } } #endregion #region Region arguments public enum RegionSource { String, Saved, RegionObject, Session, Environment, InstanceMetadata, Unknown } /// /// This interface is used for classes that need to do region resolution with the full BaseCmdlet Implementation /// public interface IAWSRegionArguments { object Region { get; } string ProfileLocation { get; } } /// /// Standalone class to resolve a region without a full BaseCmdlet implementation /// public class StandaloneRegionArguments : IAWSRegionArguments { public object Region { get; set; } public string ProfileLocation { get; set; } } /// /// This class contains the methods used to do region resolution and load a user's profile. /// public static class IAWSRegionArgumentsMethods { public static bool TryGetRegion(this IAWSRegionArguments self, bool useInstanceMetadata, out RegionEndpoint region, out RegionSource source, SessionState sessionState) { if (self == null) throw new ArgumentNullException("self"); region = null; source = RegionSource.Unknown; // user gave a command-level region parameter override? if (self.Region != null) { string regionSysName = string.Empty; if (self.Region is PSObject) { PSObject paramObject = self.Region as PSObject; if (paramObject.BaseObject is AWSRegion) regionSysName = (paramObject.BaseObject as AWSRegion).Region; else regionSysName = paramObject.BaseObject as string; } else if (self.Region is string) regionSysName = self.Region as string; if (string.IsNullOrEmpty(regionSysName)) throw new ArgumentException("Unsupported parameter type; Region must be a string containing the system name for a region, or an AWSRegion instance"); try { region = RegionEndpoint.GetBySystemName(regionSysName); source = RegionSource.String; } catch (Exception) { // be nice and informative :-) StringBuilder sb = new StringBuilder("Unsupported Region value. Supported values: "); var regions = RegionEndpoint.EnumerableAllRegions; for (int i = 0; i < regions.Count(); i++) { if (i > 0) sb.Append(","); sb.Append(regions.ElementAt(i).SystemName); } throw new ArgumentOutOfRangeException(sb.ToString()); } } // user pushed default shell variable? (this allows override of machine-wide environment setting) if (region == null && sessionState != null) { object variableValue = sessionState.PSVariable.GetValue(SessionKeys.AWSRegionVariableName); if (variableValue is string) { region = RegionEndpoint.GetBySystemName(variableValue as string); source = RegionSource.Session; } } // region set in profile store (including legacy key name)? if (region == null) { if (!TryLoadProfile(self, SettingsStore.PSDefaultSettingName, self.ProfileLocation, ref region, ref source)) TryLoadProfile(self, SettingsStore.PSLegacyDefaultSettingName, self.ProfileLocation, ref region, ref source); } // region set in environment variables? if (region == null) { try { var environmentRegion = new EnvironmentVariableAWSRegion(); region = environmentRegion.Region; source = RegionSource.Environment; } catch { } } // last chance, attempt load from EC2 instance metadata if allowed if (region == null && useInstanceMetadata) { try { region = EC2InstanceMetadata.Region; if (region != null) source = RegionSource.InstanceMetadata; } catch { } } return (region != null && source != RegionSource.Unknown); } public static bool TryLoadProfile(this IAWSRegionArguments self, string name, string profileLocation, ref RegionEndpoint region, ref RegionSource source) { CredentialProfile profile; if (SettingsStore.TryGetProfile(name, profileLocation, out profile) && profile.Region != null) { region = profile.Region; source = RegionSource.Saved; return true; } return false; } public static RegionEndpoint GetRegion(this IAWSRegionArguments self, bool useSDKFallback, SessionState sessionState) { RegionEndpoint region; RegionSource source; if (!TryGetRegion(self, useSDKFallback, out region, out source, sessionState)) region = null; return region; } } #endregion #region Common arguments internal interface IAWSCommonArguments : IAWSRegionArguments, IAWSCredentialsArguments { } internal interface IAWSCommonArgumentsFull : IAWSRegionArguments, IAWSCredentialsArgumentsFull { } #endregion #region Concrete classes public class AWSRegionArgumentsCmdlet : BaseCmdlet, IAWSRegionArguments { /// /// /// In some cases, the Region parameter collides with other parameters (e.g. RegionName on the Get-EC2Region cmdlet). /// This alias is provided to specify the Region to be used for the API call in those cases. /// /// internal const string RegionParameterAlias = "RegionToCall"; #region Parameter Region /// /// /// The system name of an AWS region or an AWSRegion instance. This governs /// the endpoint that will be used when calling service operations. Note that /// the AWS resources referenced in a call are usually region-specific. /// /// /// If the Region parameter is supplied the cmdlet ignores the ProfileLocation /// parameter. /// /// [Parameter(Mandatory = false, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, Position = 210)] [Alias(RegionParameterAlias)] public object Region { get; set; } #endregion #region Parameter ProfileLocation /// /// /// Used to specify the name and location of the ini-format credential file (shared with /// the AWS CLI and other AWS SDKs). When supplied the cmdlet will attempt to read the /// default region to be used from the credential file's default profile. /// /// /// If this optional parameter is omitted this cmdlet will search the encrypted credential /// file used by the AWS SDK for .NET and AWS Toolkit for Visual Studio first. /// If the profile is not found then the cmdlet will search in the ini-format credential /// file at the default location: (user's home directory)\.aws\credentials. /// /// /// If this parameter is specified then this cmdlet will only search the ini-format credential /// file at the location given. /// /// /// As the current folder can vary in a shell or during script execution it is advised /// that you use specify a fully qualified path instead of a relative path. /// /// /// /// Note that the encrypted credential file is not supported on all platforms. /// It will be skipped when searching for profiles on Windows Nano Server, Mac, and Linux platforms. /// [Parameter(Position = 211, ValueFromPipelineByPropertyName = true)] [Alias("AWSProfilesLocation", "ProfilesLocation")] public String ProfileLocation { get; set; } #endregion } public class AWSCommonArgumentsCmdlet : BaseCmdlet, IAWSCommonArguments { #region Parameter Region /// /// The system name of an AWS region or an AWSRegion instance. This governs /// the endpoint that will be used when calling service operations. Note that /// the AWS resources referenced in a call are usually region-specific. /// [Parameter(Mandatory = false, ValueFromPipelineByPropertyName = true)] [Alias(AWSRegionArgumentsCmdlet.RegionParameterAlias)] public object Region { get; set; } #endregion #region Parameter AccessKey /// /// The AWS access key for the user account. This can be a temporary access key /// if the corresponding session token is supplied to the -SessionToken parameter. /// [Alias("AK")] [Parameter(ValueFromPipelineByPropertyName = true)] public string AccessKey { get; set; } #endregion #region Parameter SecretKey /// /// The AWS secret key for the user account. This can be a temporary secret key /// if the corresponding session token is supplied to the -SessionToken parameter. /// [Alias("SK", "SecretAccessKey")] [Parameter(ValueFromPipelineByPropertyName = true)] public string SecretKey { get; set; } #endregion #region Parameter SessionToken /// /// The session token if the access and secret keys are temporary session-based credentials. /// [Alias("ST")] [Parameter(ValueFromPipelineByPropertyName = true)] public string SessionToken { get; set; } #endregion #region Parameter ProfileName /// /// The user-defined name of an AWS credentials or SAML-based role profile containing /// credential information. The profile is expected to be found in the secure credential /// file shared with the AWS SDK for .NET and AWS Toolkit for Visual Studio. You can also /// specify the name of a profile stored in the .ini-format credential file used with /// the AWS CLI and other AWS SDKs. /// [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("StoredCredentials", "AWSProfileName")] public string ProfileName { get; set; } #endregion #region Parameter ProfileLocation /// /// /// Used to specify the name and location of the ini-format credential file (shared with /// the AWS CLI and other AWS SDKs) /// /// /// If this optional parameter is omitted this cmdlet will search the encrypted credential /// file used by the AWS SDK for .NET and AWS Toolkit for Visual Studio first. /// If the profile is not found then the cmdlet will search in the ini-format credential /// file at the default location: (user's home directory)\.aws\credentials. /// /// /// If this parameter is specified then this cmdlet will only search the ini-format credential /// file at the location given. /// /// /// As the current folder can vary in a shell or during script execution it is advised /// that you use specify a fully qualified path instead of a relative path. /// /// /// /// Note that the encrypted credential file is not supported on all platforms. /// It will be skipped when searching for profiles on Windows Nano Server, Mac, and Linux platforms. /// [Parameter(ValueFromPipelineByPropertyName = true)] [Alias("AWSProfilesLocation", "ProfilesLocation")] public string ProfileLocation { get; set; } #endregion #region Parameter Credential /// /// An AWSCredentials object instance containing access and secret key information, /// and optionally a token for session-based credentials. /// [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public AWSCredentials Credential { get; set; } #endregion #region Parameter NetworkCredential /// /// Used with SAML-based authentication when ProfileName references a SAML role profile. /// Contains the network credentials to be supplied during authentication with the /// configured identity provider's endpoint. This parameter is not required if the /// user's default network identity can or should be used during authentication. /// [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] public PSCredential NetworkCredential { get; set; } #endregion public CredentialProfileOptions GetCredentialProfileOptions() { return new CredentialProfileOptions { AccessKey = AccessKey, SecretKey = SecretKey, Token = SessionToken, }; } } #endregion #region Helper utils public static class SettingsStore { public const string PSDefaultSettingName = "default"; public const string PSLegacyDefaultSettingName = "AWS PS Default"; public static List GetProfileInfo(string profileLocation) { var profiles = (new CredentialProfileStoreChain(profileLocation)).ListProfiles(); var result = new List(); foreach (var profile in profiles) { string location = null; var sharedCredentialsFile = profile.CredentialProfileStore as SharedCredentialsFile; if (sharedCredentialsFile == null) { var netsSDKCredentialsFile = profile.CredentialProfileStore as NetSDKCredentialsFile; if (netsSDKCredentialsFile != null) { location = null; } } else { location = sharedCredentialsFile.FilePath; } result.Add(new ProfileInfo { ProfileLocation = location, ProfileName = profile.Name, StoreTypeName = profile.CredentialProfileStore.GetType().Name }); } return result; } public static bool ProfileExists(string name, string profileLocation) { return GetProfileInfo(profileLocation).Any(pi => string.Equals(pi.ProfileName, name, StringComparison.Ordinal)); } public static bool TryGetAWSCredentials(string name, string profileLocation, out AWSCredentials credentials) { return new CredentialProfileStoreChain(profileLocation).TryGetAWSCredentials(name, out credentials); } public static bool TryGetProfile(string name, string profileLocation, out CredentialProfile profile) { return new CredentialProfileStoreChain(profileLocation).TryGetProfile(name, out profile); } public static IEnumerable ListProfiles(string profileLocation) { return new CredentialProfileStoreChain(profileLocation).ListProfiles(); } public static void RegisterProfile(CredentialProfileOptions profileOptions, string name, string profileLocation, RegionEndpoint region) { var profile = new CredentialProfile(name, profileOptions); profile.Region = region; new CredentialProfileStoreChain(profileLocation).RegisterProfile(profile); } public static void UnregisterProfile(string name, string profileLocation) { new CredentialProfileStoreChain(profileLocation).UnregisterProfile(name); } } internal static class SessionKeys { public const string AWSCredentialsVariableName = "StoredAWSCredentials"; public const string AWSRegionVariableName = "StoredAWSRegion"; public const string AWSCallHistoryName = "AWSHistory"; public const string AWSProxyVariableName = "AWSProxy"; } internal static class CredentialProfileOptionsExtractor { #pragma warning disable CS0618 //A class was marked with the Obsolete attribute private static HashSet PassThroughExtractTypes = new HashSet { typeof(InstanceProfileAWSCredentials), #if DESKTOP typeof(StoredProfileFederatedCredentials), #endif typeof(FederatedAWSCredentials), }; private static HashSet ThrowExtractTypes = new HashSet { typeof(AssumeRoleAWSCredentials), typeof(URIBasedRefreshingCredentialHelper), typeof(AnonymousAWSCredentials), typeof(ECSTaskCredentials), typeof(EnvironmentVariablesAWSCredentials), typeof(StoredProfileAWSCredentials), #if DESKTOP typeof(EnvironmentAWSCredentials), #endif typeof(EnvironmentVariablesAWSCredentials) }; #pragma warning restore CS0618 //A class was marked with the Obsolete attribute public static CredentialProfileOptions ExtractProfileOptions(AWSCredentials credentials) { var type = credentials.GetType(); if (type == typeof(BasicAWSCredentials) || type == typeof(SessionAWSCredentials)) { var immutableCredentials = credentials.GetCredentials(); return new CredentialProfileOptions { AccessKey = immutableCredentials.AccessKey, SecretKey = immutableCredentials.SecretKey, Token = immutableCredentials.Token }; } else if (PassThroughExtractTypes.Contains(type)) return null; else if (ThrowExtractTypes.Contains(type)) throw new InvalidOperationException("Cannot save credentials of type " + type.Name); else throw new InvalidOperationException("Unrecognized credentials type: " + type.Name); } } public class ProfileInfo { public string ProfileName { get; set; } public string StoreTypeName { get; set; } public string ProfileLocation { get; set; } } #endregion }