/* * 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 } }