/* * 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 AWSSDK.Runtime.Internal.Util; using System; using System.Collections.Generic; using System.Globalization; using System.Net; namespace Amazon.Runtime { /// /// Credentials that are retrieved from the Instance Profile service on an EC2 instance /// /// /// This is meant to be used when building a , as opposed /// to , which is part of the /// chain. /// public class InstanceProfileAWSCredentials : URIBasedRefreshingCredentialHelper { #region Private members // Set preempt expiry to 15 minutes. New access keys are available at least 15 minutes before expiry time. // http://docs.aws.amazon.com/IAM/latest/UserGuide/role-usecase-ec2app.html private static readonly TimeSpan _preemptExpiryTime = TimeSpan.FromMinutes(15); private static readonly TimeSpan _refreshAttemptPeriod = TimeSpan.FromHours(1); private CredentialsRefreshState _currentRefreshState = null; private readonly IWebProxy _proxy = null; private const string _receivedExpiredCredentialsFromIMDS = "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 Logger _logger; #endregion #region Properties /// /// Role for which the credentials are retrieved /// public string Role { get; set; } #endregion #region Overrides protected override CredentialsRefreshState GenerateNewCredentials() { CredentialsRefreshState newState = null; var token = EC2InstanceMetadata.FetchApiToken(); try { // Attempt to get early credentials. OK to fail at this point. newState = GetRefreshState(token); } catch (Exception e) { HttpStatusCode? httpStatusCode = ExceptionUtils.DetermineHttpStatusCode(e); if (httpStatusCode == HttpStatusCode.Unauthorized) { EC2InstanceMetadata.ClearTokenFlag(); Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "EC2 Metadata service returned unauthorized for token based secure data flow."); throw; } var logger = Logger.GetLogger(typeof(InstanceProfileAWSCredentials)); logger.InfoFormat("Error getting credentials from Instance Profile service: {0}", e); // if we already have cached credentials, we'll continue to use those credentials, // but try again to refresh them in 2 minutes. if (null != _currentRefreshState) { #pragma warning disable CS0612 // Type or member is obsolete var newExpiryTime = AWSSDKUtils.CorrectedUtcNow.ToLocalTime() + TimeSpan.FromMinutes(2); #pragma warning restore CS0612 // Type or member is obsolete _currentRefreshState = new CredentialsRefreshState(_currentRefreshState.Credentials.Copy(), newExpiryTime); return _currentRefreshState; } } if (newState?.IsExpiredWithin(TimeSpan.Zero) == true) { // special case - credentials returned are expired _logger.InfoFormat(_receivedExpiredCredentialsFromIMDS); // use a custom refresh time #pragma warning disable CS0612 // Type or member is obsolete var newExpiryTime = AWSSDKUtils.CorrectedUtcNow.ToLocalTime() + TimeSpan.FromMinutes(new Random().Next(5, 11)); #pragma warning restore CS0612 // Type or member is obsolete _currentRefreshState = new CredentialsRefreshState(newState.Credentials.Copy(), newExpiryTime); return _currentRefreshState; } // If successful, save new credentials if (newState != null) { _currentRefreshState = newState; } // If still not successful (no credentials available at start), attempt once more to // get credentials, but now without swallowing exception if (_currentRefreshState == null) { try { _currentRefreshState = GetRefreshState(token); } catch (Exception e) { HttpStatusCode? httpStatusCode = ExceptionUtils.DetermineHttpStatusCode(e); if (httpStatusCode == HttpStatusCode.Unauthorized) { EC2InstanceMetadata.ClearTokenFlag(); Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "EC2 Metadata service returned unauthorized for token based secure data flow."); } throw; } } // Return credentials that will expire in at most one hour CredentialsRefreshState state = GetEarlyRefreshState(_currentRefreshState); return state; } #endregion #region Constructors /// /// Constructs a InstanceProfileAWSCredentials object for specific role /// /// Role to use public InstanceProfileAWSCredentials(string role) : this(role, null) { } /// /// Constructs a InstanceProfileAWSCredentials object for specific role /// /// Role to use public InstanceProfileAWSCredentials(string role, IWebProxy proxy) { _logger = Logger.GetLogger(GetType()); this._proxy = proxy; if (role == null) throw new ArgumentNullException(nameof(role)); else if (IsNullOrWhiteSpace(role)) throw new ArgumentException("The argument '" + nameof(role) + "' must contain a valid role name."); Role = role; this.PreemptExpiryTime = _preemptExpiryTime; } /// /// Constructs a InstanceProfileAWSCredentials object for the first found role /// public InstanceProfileAWSCredentials() : this(proxy: null) { } /// /// Constructs a InstanceProfileAWSCredentials object for the first found role /// public InstanceProfileAWSCredentials(IWebProxy proxy) : this(GetFirstRole(proxy), proxy) { } #endregion #region Public static methods /// /// Retrieves a list of all roles available through current InstanceProfile service /// /// public static IEnumerable GetAvailableRoles() { return GetAvailableRoles(null); } /// /// Retrieves a list of all roles available through current InstanceProfile service /// /// public static IEnumerable GetAvailableRoles(IWebProxy proxy) { var token = EC2InstanceMetadata.FetchApiToken(); var allAliases = string.Empty; try { allAliases = GetContents(RolesUri, proxy, CreateMetadataTokenHeaders(token)); } catch (Exception e) { HttpStatusCode? httpStatusCode = ExceptionUtils.DetermineHttpStatusCode(e); if (httpStatusCode == HttpStatusCode.Unauthorized) { EC2InstanceMetadata.ClearTokenFlag(); Logger.GetLogger(typeof(EC2InstanceMetadata)).Error(e, "EC2 Metadata service returned unauthorized for token based secure data flow."); } throw; } if (string.IsNullOrEmpty(allAliases)) yield break; string[] parts = allAliases.Split(AliasSeparators, StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { var trim = part.Trim(); if (!string.IsNullOrEmpty(trim)) yield return trim; } } #endregion #region Private members private static string[] AliasSeparators = new string[] { "
" }; private static string Server = EC2InstanceMetadata.ServiceEndpoint; private static string RolesPath = "/latest/meta-data/iam/security-credentials/"; private static string InfoPath = "/latest/meta-data/iam/info"; private static Uri RolesUri { get { return new Uri(Server + RolesPath); } } private Uri CurrentRoleUri { get { return new Uri(Server + RolesPath + Role); } } private static Uri InfoUri { get { return new Uri(Server + InfoPath); } } private CredentialsRefreshState GetEarlyRefreshState(CredentialsRefreshState state) { // New expiry time = Now + _refreshAttemptPeriod + PreemptExpiryTime #pragma warning disable CS0612 // Type or member is obsolete var newExpiryTime = AWSSDKUtils.CorrectedUtcNow.ToLocalTime() + _refreshAttemptPeriod + PreemptExpiryTime; #pragma warning restore CS0612 // Type or member is obsolete // Use this only if the time is earlier than the default expiration time if (newExpiryTime.ToUniversalTime() > state.Expiration.ToUniversalTime()) newExpiryTime = state.Expiration; return new CredentialsRefreshState(state.Credentials.Copy(), newExpiryTime); } private CredentialsRefreshState GetRefreshState(string token) { SecurityInfo info = GetServiceInfo(_proxy, token); if (!string.IsNullOrEmpty(info.Message)) { throw new AmazonServiceException(string.Format(CultureInfo.InvariantCulture, "Unable to retrieve credentials. Message = \"{0}\".", info.Message)); } var credentials = GetRoleCredentials(token); var refreshState = new CredentialsRefreshState( new ImmutableCredentials( credentials.AccessKeyId, credentials.SecretAccessKey, credentials.Token), credentials.Expiration); return refreshState; } private static SecurityInfo GetServiceInfo(IWebProxy proxy, string token) { CheckIsIMDSEnabled(); return GetObjectFromResponse(InfoUri, proxy, CreateMetadataTokenHeaders(token)); } private SecurityCredentials GetRoleCredentials(string token) { CheckIsIMDSEnabled(); return GetObjectFromResponse(CurrentRoleUri, _proxy, CreateMetadataTokenHeaders(token)); } private static void CheckIsIMDSEnabled() { // keep this behavior consistent with GetObjectFromResponse case. if (!EC2InstanceMetadata.IsIMDSEnabled) throw new AmazonServiceException(string.Format(CultureInfo.InvariantCulture, "Unable to retrieve credentials.")); } private static string GetFirstRole() { return GetFirstRole(null); } private static string GetFirstRole(IWebProxy proxy) { IEnumerable roles = GetAvailableRoles(proxy); foreach (string role in roles) { return role; } // no roles found throw new InvalidOperationException("No roles found"); } /// /// Return true if string is null or whitespace, false otherwise. /// We can't use String.IsNullOrWhitespace because it doesn't exist /// in all frameworks we support. /// /// /// private static bool IsNullOrWhiteSpace(string s) { if (s == null) return true; else if (s.Trim().Length == 0) return true; else return false; } private static Dictionary CreateMetadataTokenHeaders(string token) { Dictionary headers = null; if (!string.IsNullOrEmpty(token)) { headers = new Dictionary(); headers.Add(HeaderKeys.XAwsEc2MetadataToken, token); } return headers; } #endregion } }