/*
* Copyright 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.
*/
using Amazon.Util;
using System;
using System.Collections;
using System.Collections.Generic;
using ThirdParty.Json.LitJson;
using System.Diagnostics;
#if AWS_ASYNC_API
using System.Threading.Tasks;
#endif
using Amazon.Runtime.Internal;
using Amazon.Runtime.Internal.Util;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
#if NETSTANDARD
using System.Runtime.InteropServices;
#endif
namespace Amazon.Runtime
{
///
/// Process Credentials can retrieve credentials by running a process and reading its stdout.
/// A new config option, "credential_process" is added to the shared config file that allows customers
/// to specify which process to run. The credentials retrieved by running this process could be either
/// Basic or Session credentials.
///
public class ProcessAWSCredentials : RefreshingAWSCredentials
{
#region Private members
private const string _versionString = "Version";
private Logger _logger = Logger.GetLogger(typeof(ProcessAWSCredentials));
private readonly ProcessStartInfo _processStartInfo;
#endregion
#region Public constructors
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
public ProcessAWSCredentials(string processCredentialInfo)
{
processCredentialInfo = processCredentialInfo.Trim();
//Default to cmd on Windows since that is the only thing BCL runs on.
var fileName = "cmd.exe";
var arguments = $@"/c {processCredentialInfo}";
#if NETSTANDARD
//If it is netstandard and not running on Windows use sh.
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
fileName = "sh";
var escapedArgs = processCredentialInfo.Replace("\\", "\\\\").Replace("\"", "\\\"");
arguments = $"-c \"{escapedArgs}\"";
}
#endif
_processStartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
CreateNoWindow = true
};
// Make sure to fetch new credentials well before the current credentials expire to avoid
// any request being made with expired credentials.
PreemptExpiryTime = TimeSpan.FromMinutes(5);
}
#endregion
#region Protected overridden methods
protected override CredentialsRefreshState GenerateNewCredentials()
{
return DetermineProcessCredential();
}
#if AWS_ASYNC_API
protected override Task GenerateNewCredentialsAsync()
{
return DetermineProcessCredentialAsync();
}
#endif
#endregion
#region Public methods
///
/// Generates new credentials by running the "credential_process" process.
///
public CredentialsRefreshState DetermineProcessCredential()
{
try
{
var processInfo = AWSSDKUtils.RunProcess(_processStartInfo);
return SetCredentialsRefreshState(processInfo);
}
catch (ProcessAWSCredentialException)
{
throw;
}
catch (Exception e)
{
_logger.DebugFormat("Process recorded exception - {0}", e);
throw new ProcessAWSCredentialException(string.Format(CultureInfo.CurrentCulture,"AWS credential process terminated with {0}", e.GetType()), e);
}
}
#if AWS_ASYNC_API
public async Task DetermineProcessCredentialAsync()
{
try
{
var processInfo = await AWSSDKUtils.RunProcessAsync(_processStartInfo).ConfigureAwait(false);
return SetCredentialsRefreshState(processInfo);
}
catch (ProcessAWSCredentialException)
{
throw;
}
catch (Exception e)
{
_logger.DebugFormat("Process recorded exception - {0}", e);
throw new ProcessAWSCredentialException(string.Format(CultureInfo.CurrentCulture,"AWS credential process terminated with {0}", e.GetType()), e);
}
}
#endif
#endregion
#region Private methods
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
private CredentialsRefreshState SetCredentialsRefreshState(ProcessExecutionResult processInfo)
{
_logger.InfoFormat("Process ends with exitcode - {0}", processInfo.ExitCode);
// Setting useShellExecute to false enables the user to just specify the executable name.
// The system will attempt to find the executable within folders specified by the PATH environment variable.
if (processInfo.ExitCode == 0)
{
JsonData data = null;
try
{
data = JsonMapper.ToObject(processInfo.StandardOutput);
}
catch(JsonException je)
{
throw new ProcessAWSCredentialException("The response back from the process credential provider returned back a malformed JSON document.", je);
}
if (!data.PropertyNames.Contains(_versionString)|| string.IsNullOrEmpty(data[_versionString].ToString()))
{
throw new ProcessAWSCredentialException("Missing required parameter - Version in JSON Payload");
}
var version = (int)data[_versionString];
switch (version)
{
case 1:
ProcessCredentialVersion1 processCredentialDataV1 = null;
try
{
processCredentialDataV1 = JsonMapper.ToObject(processInfo.StandardOutput);
}
catch (Exception e)
{
throw new ProcessAWSCredentialException("The response back from the process credential provider returned back a malformed JSON document.", e);
}
return new CredentialsRefreshState(
new ImmutableCredentials(processCredentialDataV1.AccessKeyId,
processCredentialDataV1.SecretAccessKey, processCredentialDataV1.SessionToken),processCredentialDataV1.Expiration);
default:
throw new ProcessAWSCredentialException(string.Format(CultureInfo.CurrentCulture,"Unsupported credential version: {0}" + version));
}
}
var processException = new ProcessAWSCredentialException(string.Format(CultureInfo.CurrentCulture, "Command returned non-zero exit value {0} with the error - {1}", processInfo.ExitCode, processInfo.StandardError));
_logger.DebugFormat("Process {0} recorded exception - {1}", _processStartInfo.FileName, processException);
throw processException;
}
#endregion
}
///
/// Exception class to capture all exceptions encountered when starting or
/// executing the "credential_process" process. If the user has specified an executable
/// and the SDK is unable to execute it, the exception should be surfaces to the user
/// instead of moving on to the next credential provider.
///
#if !NETSTANDARD
[Serializable]
#endif
public class ProcessAWSCredentialException : Exception
{
public ProcessAWSCredentialException(string message) : base(message)
{
}
public ProcessAWSCredentialException(string message, Exception inner)
: base(message, inner)
{
}
#if !NETSTANDARD
protected ProcessAWSCredentialException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
: base(info, context)
{
}
#endif
}
}