1 /*
2  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15
16 package software.amazon.awssdk.auth.credentials;
17
18 import com.fasterxml.jackson.databind.JsonMappingException;
19 import com.fasterxml.jackson.databind.JsonNode;
20 import java.io.IOException;
21 import java.time.Duration;
22 import java.time.Instant;
23 import java.util.Optional;
24 import software.amazon.awssdk.annotations.SdkProtectedApi;
25 import software.amazon.awssdk.core.exception.SdkClientException;
26 import software.amazon.awssdk.core.util.json.JacksonUtils;
27 import software.amazon.awssdk.regions.util.HttpResourcesUtils;
28 import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
29 import software.amazon.awssdk.utils.ComparableUtils;
30 import software.amazon.awssdk.utils.DateUtils;
31 import software.amazon.awssdk.utils.SdkAutoCloseable;
32 import software.amazon.awssdk.utils.Validate;
33 import software.amazon.awssdk.utils.cache.CachedSupplier;
34 import software.amazon.awssdk.utils.cache.NonBlocking;
35 import software.amazon.awssdk.utils.cache.RefreshResult;
36
37 /**
38  * Helper class that contains the common behavior of the CredentialsProviders that loads the credentials from a local endpoint on
39  * a container (e.g. an EC2 instance).
40  */

41 @SdkProtectedApi
42 public abstract class HttpCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable {
43     private final Optional<CachedSupplier<AwsCredentials>> credentialsCache;
44
45     protected HttpCredentialsProvider(BuilderImpl<?, ?> builder) {
46         this(builder.asyncCredentialUpdateEnabled, builder.asyncThreadName);
47     }
48
49     HttpCredentialsProvider(boolean asyncCredentialUpdateEnabled, String asyncThreadName) {
50         if (isLocalCredentialLoadingDisabled()) {
51             this.credentialsCache = Optional.empty();
52         } else {
53             CachedSupplier.Builder<AwsCredentials> cacheBuilder = CachedSupplier.builder(this::refreshCredentials);
54             if (asyncCredentialUpdateEnabled) {
55                 cacheBuilder.prefetchStrategy(new NonBlocking(asyncThreadName));
56             }
57             this.credentialsCache = Optional.of(cacheBuilder.build());
58         }
59     }
60
61     protected abstract ResourcesEndpointProvider getCredentialsEndpointProvider();
62
63     /**
64      * Can be overridden by subclass to decide whether loading credential is disabled or not.
65      *
66      * @return whether loading credential from local endpoint is disabled.
67      */

68     protected boolean isLocalCredentialLoadingDisabled() {
69         return false;
70     }
71
72     private RefreshResult<AwsCredentials> refreshCredentials() {
73         try {
74             String credentialsResponse = HttpResourcesUtils.instance().readResource(getCredentialsEndpointProvider());
75
76             JsonNode node = JacksonUtils.sensitiveJsonNodeOf(credentialsResponse);
77             JsonNode accessKey = node.get("AccessKeyId");
78             JsonNode secretKey = node.get("SecretAccessKey");
79             JsonNode token = node.get("Token");
80             JsonNode expirationNode = node.get("Expiration");
81
82             Validate.notNull(accessKey, "Failed to load access key.");
83             Validate.notNull(secretKey, "Failed to load secret key.");
84
85             AwsCredentials credentials =
86                 token == null ? AwsBasicCredentials.create(accessKey.asText(), secretKey.asText())
87                               : AwsSessionCredentials.create(accessKey.asText(), secretKey.asText(), token.asText());
88
89             Instant expiration = getExpiration(expirationNode).orElse(null);
90             if (expiration != null && Instant.now().isAfter(expiration)) {
91                 throw SdkClientException.builder()
92                                         .message("Credentials obtained from metadata service are already expired.")
93                                         .build();
94             }
95             return RefreshResult.builder(credentials)
96                                 .staleTime(getStaleTime(expiration))
97                                 .prefetchTime(getPrefetchTime(expiration))
98                                 .build();
99         } catch (SdkClientException e) {
100             throw e;
101         } catch (JsonMappingException e) {
102             throw SdkClientException.builder()
103                                     .message("Unable to parse response returned from service endpoint.")
104                                     .cause(e)
105                                     .build();
106         } catch (RuntimeException | IOException e) {
107             throw SdkClientException.builder()
108                                     .message("Unable to load credentials from service endpoint.")
109                                     .cause(e)
110                                     .build();
111         }
112     }
113
114     private Optional<Instant> getExpiration(JsonNode expirationNode) {
115         return Optional.ofNullable(expirationNode).map(node -> {
116             // Convert the expirationNode string to ISO-8601 format.
117             String expirationValue = node.asText().replaceAll("\\+0000$""Z");
118
119             try {
120                 return DateUtils.parseIso8601Date(expirationValue);
121             } catch (RuntimeException e) {
122                 throw new IllegalStateException("Unable to parse credentials expiration date from metadata service.", e);
123             }
124         });
125     }
126
127     private Instant getStaleTime(Instant expiration) {
128         return expiration == null ? null
129                                   : expiration.minus(Duration.ofMinutes(1));
130     }
131
132     private Instant getPrefetchTime(Instant expiration) {
133         Instant oneHourFromNow = Instant.now().plus(Duration.ofHours(1));
134         return expiration == null ? oneHourFromNow
135                                   : ComparableUtils.minimum(oneHourFromNow, expiration.minus(Duration.ofMinutes(15)));
136     }
137
138     @Override
139     public AwsCredentials resolveCredentials() {
140         if (isLocalCredentialLoadingDisabled()) {
141             throw SdkClientException.builder()
142                                     .message("Loading credentials from local endpoint is disabled. Unable to load " +
143                                              "credentials from service endpoint.")
144                                     .build();
145         }
146         return credentialsCache.map(CachedSupplier::get).orElseThrow(() ->
147                 SdkClientException.builder().message("Unable to load credentials from service endpoint").build());
148     }
149
150     @Override
151     public void close() {
152         credentialsCache.ifPresent(CachedSupplier::close);
153     }
154
155     public interface Builder<TypeToBuildT extends HttpCredentialsProvider, BuilderT extends Builder> {
156         /**
157          * Configure whether this provider should fetch credentials asynchronously in the background. If this is true, threads are
158          * less likely to block when {@link #resolveCredentials()} is called, but additional resources are used to maintain the
159          * provider.
160          *
161          * <p>
162          * By defaultthis is disabled.
163          */

164         BuilderT asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled);
165
166         BuilderT asyncThreadName(String asyncThreadName);
167
168         TypeToBuildT build();
169     }
170
171     /**
172      * A builder for creating a custom a {@link InstanceProfileCredentialsProvider}.
173      */

174     protected abstract static class BuilderImpl<TypeToBuildT extends HttpCredentialsProvider, BuilderT extends Builder>
175         implements Builder<TypeToBuildT, BuilderT> {
176         private boolean asyncCredentialUpdateEnabled = false;
177         private String asyncThreadName;
178
179         protected BuilderImpl() {
180         }
181
182         @Override
183         public BuilderT asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled) {
184             this.asyncCredentialUpdateEnabled = asyncCredentialUpdateEnabled;
185             return (BuilderT) this;
186         }
187
188         public void setAsyncCredentialUpdateEnabled(boolean asyncCredentialUpdateEnabled) {
189             asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled);
190         }
191
192         @Override
193         public BuilderT asyncThreadName(String asyncThreadName) {
194             this.asyncThreadName = asyncThreadName;
195             return (BuilderT) this;
196         }
197
198         public void setAsyncThreadName(String asyncThreadName) {
199             asyncThreadName(asyncThreadName);
200         }
201     }
202 }
203