/*
* 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.Runtime.Internal.Util;
using Amazon.Util;
using System;
using System.Globalization;
using System.Threading;
#if AWS_ASYNC_API
using System.Threading.Tasks;
#endif
namespace Amazon.Runtime
{
///
/// InstanceProfileAWSCredentials allows configuring Roles and every instance hits IMDS individually.
/// This class has a singleton timer task that caches instance profile credentials every 2 minutes.
///
internal class DefaultInstanceProfileAWSCredentials : AWSCredentials, IDisposable
{
private static readonly object _instanceLock = new object();
private readonly ReaderWriterLockSlim _credentialsLock = new ReaderWriterLockSlim(); // Lock to control getting credentials across multiple threads.
private readonly Timer _credentialsRetrieverTimer;
private RefreshingAWSCredentials.CredentialsRefreshState _lastRetrievedCredentials;
private Logger _logger;
private static readonly TimeSpan _neverTimespan = TimeSpan.FromMilliseconds(-1);
private static readonly TimeSpan _refreshRate = TimeSpan.FromMinutes(2); // EC2 refreshes credentials 5 min before expiration
private const string FailedToGetCredentialsMessage = "Failed to retrieve credentials from EC2 Instance Metadata Service.";
private static readonly TimeSpan _credentialsLockTimeout = TimeSpan.FromSeconds(5);
///
/// Control flag: in the event IMDS returns an expired credential, a refresh must be immediately
/// retried, if it continues to fail, then retry every 5-10 minutes.
///
private static volatile bool _imdsRefreshFailed = false;
private const string _usingExpiredCredentialsFromIMDS =
"Attempting credential expiration extension due to a credential service availability issue. " +
"A refresh of these credentials will be attempted again in 5-10 minutes.";
private static DefaultInstanceProfileAWSCredentials _instance;
public static DefaultInstanceProfileAWSCredentials Instance
{
get
{
CheckIsIMDSEnabled();
if (_instance == null)
{
lock (_instanceLock)
{
if (_instance == null)
{
_instance = new DefaultInstanceProfileAWSCredentials();
}
}
}
return _instance;
}
}
private DefaultInstanceProfileAWSCredentials()
{
// if IMDS is turned off, no need to spin up the timer task
if (!EC2InstanceMetadata.IsIMDSEnabled) return;
_logger = Logger.GetLogger(typeof(DefaultInstanceProfileAWSCredentials));
_credentialsRetrieverTimer = new Timer(RenewCredentials, null, TimeSpan.Zero, _neverTimespan);
}
#region Overrides
///
/// Returns a copy of the most recent instance profile credentials.
///
public override ImmutableCredentials GetCredentials()
{
CheckIsIMDSEnabled();
ImmutableCredentials credentials = null;
// Try to acquire read lock. The thread would be blocked if another thread has write lock.
if (_credentialsLock.TryEnterReadLock(_credentialsLockTimeout))
{
try
{
if (null != _lastRetrievedCredentials)
{
if (_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero) &&
!_imdsRefreshFailed)
{
// this is the first failure - immediately try to renew
_imdsRefreshFailed = true;
_lastRetrievedCredentials = FetchCredentials();
}
// if credentials are expired, we'll still return them, but log a message about
// them being expired.
if (_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero))
{
_logger.InfoFormat(_usingExpiredCredentialsFromIMDS);
}
else
{
_imdsRefreshFailed = false;
}
return _lastRetrievedCredentials?.Credentials.Copy();
}
}
finally
{
_credentialsLock.ExitReadLock();
}
}
// If there's no credentials cached, hit IMDS directly. Try to acquire write lock.
if (_credentialsLock.TryEnterWriteLock(_credentialsLockTimeout))
{
try
{
// Check for last retrieved credentials again in case other thread might have already fetched it.
if (null == _lastRetrievedCredentials)
{
_lastRetrievedCredentials = FetchCredentials();
}
if (_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero) &&
!_imdsRefreshFailed)
{
// this is the first failure - immediately try to renew
_imdsRefreshFailed = true;
_lastRetrievedCredentials = FetchCredentials();
}
// if credentials are expired, we'll still return them, but log a message about
// them being expired.
if (_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero))
{
_logger.InfoFormat(_usingExpiredCredentialsFromIMDS);
}
else
{
_imdsRefreshFailed = false;
}
credentials = _lastRetrievedCredentials.Credentials?.Copy();
}
finally
{
_credentialsLock.ExitWriteLock();
}
}
if (credentials == null)
{
throw new AmazonServiceException(FailedToGetCredentialsMessage);
}
return credentials;
}
#if AWS_ASYNC_API
///
/// Returns a copy of the most recent instance profile credentials.
///
public override Task GetCredentialsAsync()
{
return Task.FromResult(GetCredentials());
}
#endif
#endregion
#region Private members
private void RenewCredentials(object unused)
{
// By default, the refreshRate will continue to be
// _refreshRate, but if FetchCredentials() returns an expired credential,
// the refresh rate will be adjusted
var refreshRate = _refreshRate;
try
{
// if FetchCredentials() call were to fail, _lastRetrievedCredentials
// would remain unchanged and would continue to be returned in GetCredentials()
_lastRetrievedCredentials = FetchCredentials();
// check for a first time failure
if (!_imdsRefreshFailed &&
_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero))
{
// this is the first failure - immediately try to renew
_imdsRefreshFailed = true;
_lastRetrievedCredentials = FetchCredentials();
}
// first failure refresh failed OR subsequent refresh failed.
if (_lastRetrievedCredentials.IsExpiredWithin(TimeSpan.Zero))
{
// relax the refresh rate to at least 5 minutes
refreshRate = TimeSpan.FromMinutes(new Random().Next(5, 11));
}
else
{
_imdsRefreshFailed = false;
}
}
catch (OperationCanceledException e)
{
_logger.Error(e, "RenewCredentials task canceled");
}
catch (Exception e)
{
// we want to suppress any exceptions from this timer task.
_logger.Error(e, FailedToGetCredentialsMessage);
}
finally
{
// re-invoke this task once after time specified by refreshRate set at beginning of this method
_credentialsRetrieverTimer.Change(refreshRate, _neverTimespan);
}
}
private static RefreshingAWSCredentials.CredentialsRefreshState FetchCredentials()
{
var securityCredentials = EC2InstanceMetadata.IAMSecurityCredentials;
if (securityCredentials == null)
throw new AmazonServiceException("Unable to get IAM security credentials from EC2 Instance Metadata Service.");
string firstRole = null;
foreach (var role in securityCredentials.Keys)
{
firstRole = role;
break;
}
if (string.IsNullOrEmpty(firstRole))
throw new AmazonServiceException("Unable to get EC2 instance role from EC2 Instance Metadata Service.");
var metadata = securityCredentials[firstRole];
if (metadata == null)
throw new AmazonServiceException("Unable to get credentials for role \"" + firstRole + "\" from EC2 Instance Metadata Service.");
return new RefreshingAWSCredentials.CredentialsRefreshState(
new ImmutableCredentials(metadata.AccessKeyId, metadata.SecretAccessKey, metadata.Token),
metadata.Expiration);
}
private static void CheckIsIMDSEnabled()
{
// keep this behavior consistent with GetObjectFromResponse case.
if (!EC2InstanceMetadata.IsIMDSEnabled) throw new AmazonServiceException("Unable to retrieve credentials.");
}
#endregion
#region IDisposable Support
private bool _isDisposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
lock (_instanceLock)
{
_credentialsRetrieverTimer.Dispose();
_logger = null;
_instance = null;
}
}
_isDisposed = true;
}
}
///
/// Dispose this object and all related resources.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}