/*
* 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;
namespace Amazon.Runtime
{
///
/// Abstract class for automatically refreshing AWS credentials
///
public abstract class RefreshingAWSCredentials : AWSCredentials, IDisposable
{
private Logger _logger = Logger.GetLogger(typeof(RefreshingAWSCredentials));
#region Refresh data
///
/// Refresh state container consisting of credentials
/// and the date of the their expiration
///
public class CredentialsRefreshState
{
public ImmutableCredentials Credentials
{
get;
set;
}
public DateTime Expiration { get; set; }
public CredentialsRefreshState()
{
}
public CredentialsRefreshState(ImmutableCredentials credentials, DateTime expiration)
{
Credentials = credentials;
Expiration = expiration;
}
internal bool IsExpiredWithin(TimeSpan preemptExpiryTime)
{
#pragma warning disable CS0612 // Type or member is obsolete
var now = AWSSDKUtils.CorrectedUtcNow;
#pragma warning restore CS0612 // Type or member is obsolete
var exp = Expiration.ToUniversalTime();
return (now > exp - preemptExpiryTime);
}
}
///
/// Represents the current state of the Credentials.
///
/// This can be cleared without synchronization.
protected CredentialsRefreshState currentState;
#region Private members
private TimeSpan _preemptExpiryTime = TimeSpan.FromMinutes(0);
private bool _disposed;
#if BCL35
///
/// Semaphore to control thread access to GetCredentialsAsync method.
/// The semaphore will allow only one thread to generate new credentials and
/// update the current state.
///
private readonly Semaphore _updateGeneratedCredentialsSemaphore = new Semaphore(1, 1);
#else
///
/// Semaphore to control thread access to GetCredentialsAsync method.
/// The semaphore will allow only one thread to generate new credentials and
/// update the current state.
///
private readonly SemaphoreSlim _updateGeneratedCredentialsSemaphore = new SemaphoreSlim(1, 1);
#endif
#endregion
#endregion
#region Properties
///
/// The time before actual expiration to expire the credentials.
/// Property cannot be set to a negative TimeSpan.
///
public TimeSpan PreemptExpiryTime
{
get { return _preemptExpiryTime; }
set
{
if (value < TimeSpan.Zero)
throw new ArgumentOutOfRangeException("value", "PreemptExpiryTime cannot be negative");
_preemptExpiryTime = value;
}
}
#endregion
#region Override methods
///
/// Returns an instance of ImmutableCredentials for this instance
///
///
public override ImmutableCredentials GetCredentials()
{
_updateGeneratedCredentialsSemaphore.Wait();
try
{
// We save the currentState as it might be modified or cleared.
var tempState = currentState;
// If credentials are expired or we don't have any state yet, update
if (ShouldUpdateState(tempState, PreemptExpiryTime))
{
tempState = GenerateNewCredentials();
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
}
return tempState.Credentials.Copy();
}
finally
{
_updateGeneratedCredentialsSemaphore.Release();
}
}
#if AWS_ASYNC_API
public override async System.Threading.Tasks.Task GetCredentialsAsync()
{
await _updateGeneratedCredentialsSemaphore.WaitAsync().ConfigureAwait(false);
try
{
// We save the currentState as it might be modified or cleared.
var tempState = currentState;
// If credentials are expired, update
if (ShouldUpdateState(tempState, PreemptExpiryTime))
{
tempState = await GenerateNewCredentialsAsync().ConfigureAwait(false);
UpdateToGeneratedCredentials(tempState, PreemptExpiryTime);
currentState = tempState;
}
return tempState.Credentials.Copy();
}
finally
{
_updateGeneratedCredentialsSemaphore.Release();
}
}
#endif
#endregion
#region Private/protected credential update methods
private static void UpdateToGeneratedCredentials(CredentialsRefreshState state, TimeSpan preemptExpiryTime)
{
// Check if the new credentials are already expired
if (ShouldUpdateState(state, preemptExpiryTime))
{
string errorMessage;
if (state == null)
{
errorMessage = "Unable to generate temporary credentials";
}
else
{
errorMessage = string.Format(CultureInfo.InvariantCulture,
"The retrieved credentials have already expired: Now = {0}, Credentials expiration = {1}",
#pragma warning disable CS0612 // Type or member is obsolete
AWSSDKUtils.CorrectedUtcNow.ToLocalTime(), state.Expiration);
#pragma warning restore CS0612 // Type or member is obsolete
}
throw new AmazonClientException(errorMessage);
}
// Offset the Expiration by PreemptExpiryTime. This produces the expiration window
// where the credentials should be updated before they actually expire.
state.Expiration -= preemptExpiryTime;
if (ShouldUpdateState(state, preemptExpiryTime))
{
// This could happen if the default value of PreemptExpiryTime is
// overridden and set too high such that ShouldUpdate returns true.
var logger = Logger.GetLogger(typeof(RefreshingAWSCredentials));
logger.InfoFormat(
"The preempt expiry time is set too high: Current time = {0}, Credentials expiry time = {1}, Preempt expiry time = {2}.",
#pragma warning disable CS0612 // Type or member is obsolete
AWSSDKUtils.CorrectedUtcNow.ToLocalTime(),
#pragma warning restore CS0612 // Type or member is obsolete
state.Expiration, preemptExpiryTime);
}
}
///
/// Test credentials existence and expiration time
/// should update if:
/// credentials have not been loaded yet
/// it's past the expiration time. At this point currentState.Expiration may
/// have the PreemptExpiryTime baked into to the expiration from a call to
/// UpdateToGeneratedCredentials but it may not if this is new application load.
///
protected bool ShouldUpdate
{
get
{
return ShouldUpdateState(currentState, PreemptExpiryTime);
}
}
// Test credentials existence and expiration time
// should update if:
// credentials have not been loaded yet
// it's past the expiration time. At this point currentState.Expiration may
// have the PreemptExpiryTime baked into to the expiration from a call to
// UpdateToGeneratedCredentials but it may not if this is new application
// load.
private static bool ShouldUpdateState(CredentialsRefreshState state, TimeSpan preemptExpiryTime)
{
// it's past the expiration time. At this point currentState.Expiration may
// have the PreemptExpiryTime baked into to the expiration from a call to
// UpdateToGeneratedCredentials but it may not if this is new application
// load.
var isExpired = state?.IsExpiredWithin(TimeSpan.Zero);
if (isExpired == true)
{
#pragma warning disable CS0612 // Type or member is obsolete
var logger = Logger.GetLogger(typeof(RefreshingAWSCredentials));
logger.InfoFormat("Determined refreshing credentials should update. Expiration time: {0}, Current time: {1}",
state.Expiration.Add(preemptExpiryTime).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture),
AWSSDKUtils.CorrectedUtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture));
#pragma warning restore CS0612 // Type or member is obsolete
}
return isExpired ?? true;
}
///
/// When overridden in a derived class, generates new credentials and new expiration date.
///
/// Called on first credentials request and when expiration date is in the past.
///
///
protected virtual CredentialsRefreshState GenerateNewCredentials()
{
throw new NotImplementedException();
}
#if AWS_ASYNC_API
///
/// When overridden in a derived class, generates new credentials and new expiration date.
///
/// Called on first credentials request and when expiration date is in the past.
///
///
protected virtual System.Threading.Tasks.Task GenerateNewCredentialsAsync()
{
return System.Threading.Tasks.Task.Run(() => this.GenerateNewCredentials());
}
#endif
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
_updateGeneratedCredentialsSemaphore.Dispose();
}
_disposed = true;
}
#endregion
#region Public Methods
///
/// Clears currently-stored credentials, forcing the next GetCredentials call to generate new credentials.
///
public virtual void ClearCredentials()
{
currentState = null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}