• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //
2 // Copyright 2020 gRPC authors.
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 // You may obtain a copy of the License at
7 //
8 //     http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 #include <grpc/support/port_platform.h>
17 
18 #include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
19 
20 #include "absl/strings/str_format.h"
21 #include "absl/strings/str_join.h"
22 #include "absl/strings/str_replace.h"
23 
24 #include "src/core/lib/gpr/env.h"
25 
26 namespace grpc_core {
27 
28 namespace {
29 
30 const char* kExpectedEnvironmentId = "aws1";
31 
32 const char* kRegionEnvVar = "AWS_REGION";
33 const char* kDefaultRegionEnvVar = "AWS_DEFAULT_REGION";
34 const char* kAccessKeyIdEnvVar = "AWS_ACCESS_KEY_ID";
35 const char* kSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY";
36 const char* kSessionTokenEnvVar = "AWS_SESSION_TOKEN";
37 
UrlEncode(const absl::string_view & s)38 std::string UrlEncode(const absl::string_view& s) {
39   const char* hex = "0123456789ABCDEF";
40   std::string result;
41   result.reserve(s.length());
42   for (auto c : s) {
43     if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
44         (c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
45         c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
46       result.push_back(c);
47     } else {
48       result.push_back('%');
49       result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
50       result.push_back(hex[static_cast<unsigned char>(c) & 15]);
51     }
52   }
53   return result;
54 }
55 
56 }  // namespace
57 
58 RefCountedPtr<AwsExternalAccountCredentials>
Create(Options options,std::vector<std::string> scopes,grpc_error_handle * error)59 AwsExternalAccountCredentials::Create(Options options,
60                                       std::vector<std::string> scopes,
61                                       grpc_error_handle* error) {
62   auto creds = MakeRefCounted<AwsExternalAccountCredentials>(
63       std::move(options), std::move(scopes), error);
64   if (*error == GRPC_ERROR_NONE) {
65     return creds;
66   } else {
67     return nullptr;
68   }
69 }
70 
AwsExternalAccountCredentials(Options options,std::vector<std::string> scopes,grpc_error_handle * error)71 AwsExternalAccountCredentials::AwsExternalAccountCredentials(
72     Options options, std::vector<std::string> scopes, grpc_error_handle* error)
73     : ExternalAccountCredentials(options, std::move(scopes)) {
74   audience_ = options.audience;
75   auto it = options.credential_source.object_value().find("environment_id");
76   if (it == options.credential_source.object_value().end()) {
77     *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
78         "environment_id field not present.");
79     return;
80   }
81   if (it->second.type() != Json::Type::STRING) {
82     *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
83         "environment_id field must be a string.");
84     return;
85   }
86   if (it->second.string_value() != kExpectedEnvironmentId) {
87     *error =
88         GRPC_ERROR_CREATE_FROM_STATIC_STRING("environment_id does not match.");
89     return;
90   }
91   it = options.credential_source.object_value().find("region_url");
92   if (it == options.credential_source.object_value().end()) {
93     *error =
94         GRPC_ERROR_CREATE_FROM_STATIC_STRING("region_url field not present.");
95     return;
96   }
97   if (it->second.type() != Json::Type::STRING) {
98     *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
99         "region_url field must be a string.");
100     return;
101   }
102   region_url_ = it->second.string_value();
103   it = options.credential_source.object_value().find("url");
104   if (it != options.credential_source.object_value().end() &&
105       it->second.type() == Json::Type::STRING) {
106     url_ = it->second.string_value();
107   }
108   it = options.credential_source.object_value().find(
109       "regional_cred_verification_url");
110   if (it == options.credential_source.object_value().end()) {
111     *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
112         "regional_cred_verification_url field not present.");
113     return;
114   }
115   if (it->second.type() != Json::Type::STRING) {
116     *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
117         "regional_cred_verification_url field must be a string.");
118     return;
119   }
120   regional_cred_verification_url_ = it->second.string_value();
121 }
122 
RetrieveSubjectToken(HTTPRequestContext * ctx,const Options &,std::function<void (std::string,grpc_error_handle)> cb)123 void AwsExternalAccountCredentials::RetrieveSubjectToken(
124     HTTPRequestContext* ctx, const Options& /*options*/,
125     std::function<void(std::string, grpc_error_handle)> cb) {
126   if (ctx == nullptr) {
127     FinishRetrieveSubjectToken(
128         "",
129         GRPC_ERROR_CREATE_FROM_STATIC_STRING(
130             "Missing HTTPRequestContext to start subject token retrieval."));
131     return;
132   }
133   ctx_ = ctx;
134   cb_ = cb;
135   if (signer_ != nullptr) {
136     BuildSubjectToken();
137   } else {
138     RetrieveRegion();
139   }
140 }
141 
RetrieveRegion()142 void AwsExternalAccountCredentials::RetrieveRegion() {
143   UniquePtr<char> region_from_env(gpr_getenv(kRegionEnvVar));
144   if (region_from_env == nullptr) {
145     region_from_env = UniquePtr<char>(gpr_getenv(kDefaultRegionEnvVar));
146   }
147   if (region_from_env != nullptr) {
148     region_ = std::string(region_from_env.get());
149     if (url_.empty()) {
150       RetrieveSigningKeys();
151     } else {
152       RetrieveRoleName();
153     }
154     return;
155   }
156   absl::StatusOr<URI> uri = URI::Parse(region_url_);
157   if (!uri.ok()) {
158     FinishRetrieveSubjectToken("", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
159                                        absl::StrFormat("Invalid region url. %s",
160                                                        uri.status().ToString())
161                                            .c_str()));
162     return;
163   }
164   grpc_httpcli_request request;
165   memset(&request, 0, sizeof(grpc_httpcli_request));
166   request.host = const_cast<char*>(uri->authority().c_str());
167   request.http.path = gpr_strdup(uri->path().c_str());
168   request.handshaker =
169       uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
170   grpc_resource_quota* resource_quota =
171       grpc_resource_quota_create("external_account_credentials");
172   grpc_http_response_destroy(&ctx_->response);
173   ctx_->response = {};
174   GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRegion, this, nullptr);
175   grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
176                    &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
177   grpc_resource_quota_unref_internal(resource_quota);
178   grpc_http_request_destroy(&request.http);
179 }
180 
OnRetrieveRegion(void * arg,grpc_error_handle error)181 void AwsExternalAccountCredentials::OnRetrieveRegion(void* arg,
182                                                      grpc_error_handle error) {
183   AwsExternalAccountCredentials* self =
184       static_cast<AwsExternalAccountCredentials*>(arg);
185   self->OnRetrieveRegionInternal(GRPC_ERROR_REF(error));
186 }
187 
OnRetrieveRegionInternal(grpc_error_handle error)188 void AwsExternalAccountCredentials::OnRetrieveRegionInternal(
189     grpc_error_handle error) {
190   if (error != GRPC_ERROR_NONE) {
191     FinishRetrieveSubjectToken("", error);
192     return;
193   }
194   // Remove the last letter of availability zone to get pure region
195   absl::string_view response_body(ctx_->response.body,
196                                   ctx_->response.body_length);
197   region_ = std::string(response_body.substr(0, response_body.size() - 1));
198   if (url_.empty()) {
199     RetrieveSigningKeys();
200   } else {
201     RetrieveRoleName();
202   }
203 }
204 
RetrieveRoleName()205 void AwsExternalAccountCredentials::RetrieveRoleName() {
206   absl::StatusOr<URI> uri = URI::Parse(url_);
207   if (!uri.ok()) {
208     FinishRetrieveSubjectToken(
209         "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
210                 absl::StrFormat("Invalid url: %s.", uri.status().ToString())
211                     .c_str()));
212     return;
213   }
214   grpc_httpcli_request request;
215   memset(&request, 0, sizeof(grpc_httpcli_request));
216   request.host = const_cast<char*>(uri->authority().c_str());
217   request.http.path = gpr_strdup(uri->path().c_str());
218   request.handshaker =
219       uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
220   grpc_resource_quota* resource_quota =
221       grpc_resource_quota_create("external_account_credentials");
222   grpc_http_response_destroy(&ctx_->response);
223   ctx_->response = {};
224   GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRoleName, this, nullptr);
225   grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
226                    &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
227   grpc_resource_quota_unref_internal(resource_quota);
228   grpc_http_request_destroy(&request.http);
229 }
230 
OnRetrieveRoleName(void * arg,grpc_error_handle error)231 void AwsExternalAccountCredentials::OnRetrieveRoleName(
232     void* arg, grpc_error_handle error) {
233   AwsExternalAccountCredentials* self =
234       static_cast<AwsExternalAccountCredentials*>(arg);
235   self->OnRetrieveRoleNameInternal(GRPC_ERROR_REF(error));
236 }
237 
OnRetrieveRoleNameInternal(grpc_error_handle error)238 void AwsExternalAccountCredentials::OnRetrieveRoleNameInternal(
239     grpc_error_handle error) {
240   if (error != GRPC_ERROR_NONE) {
241     FinishRetrieveSubjectToken("", error);
242     return;
243   }
244   role_name_ = std::string(ctx_->response.body, ctx_->response.body_length);
245   RetrieveSigningKeys();
246 }
247 
RetrieveSigningKeys()248 void AwsExternalAccountCredentials::RetrieveSigningKeys() {
249   UniquePtr<char> access_key_id_from_env(gpr_getenv(kAccessKeyIdEnvVar));
250   UniquePtr<char> secret_access_key_from_env(
251       gpr_getenv(kSecretAccessKeyEnvVar));
252   UniquePtr<char> token_from_env(gpr_getenv(kSessionTokenEnvVar));
253   if (access_key_id_from_env != nullptr &&
254       secret_access_key_from_env != nullptr && token_from_env != nullptr) {
255     access_key_id_ = std::string(access_key_id_from_env.get());
256     secret_access_key_ = std::string(secret_access_key_from_env.get());
257     token_ = std::string(token_from_env.get());
258     BuildSubjectToken();
259     return;
260   }
261   if (role_name_.empty()) {
262     FinishRetrieveSubjectToken(
263         "", GRPC_ERROR_CREATE_FROM_STATIC_STRING(
264                 "Missing role name when retrieving signing keys."));
265     return;
266   }
267   std::string url_with_role_name = absl::StrCat(url_, "/", role_name_);
268   absl::StatusOr<URI> uri = URI::Parse(url_with_role_name);
269   if (!uri.ok()) {
270     FinishRetrieveSubjectToken(
271         "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
272                 absl::StrFormat("Invalid url with role name: %s.",
273                                 uri.status().ToString())
274                     .c_str()));
275     return;
276   }
277   grpc_httpcli_request request;
278   memset(&request, 0, sizeof(grpc_httpcli_request));
279   request.host = const_cast<char*>(uri->authority().c_str());
280   request.http.path = gpr_strdup(uri->path().c_str());
281   request.handshaker =
282       uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
283   grpc_resource_quota* resource_quota =
284       grpc_resource_quota_create("external_account_credentials");
285   grpc_http_response_destroy(&ctx_->response);
286   ctx_->response = {};
287   GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveSigningKeys, this, nullptr);
288   grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
289                    &request, ctx_->deadline, &ctx_->closure, &ctx_->response);
290   grpc_resource_quota_unref_internal(resource_quota);
291   grpc_http_request_destroy(&request.http);
292 }
293 
OnRetrieveSigningKeys(void * arg,grpc_error_handle error)294 void AwsExternalAccountCredentials::OnRetrieveSigningKeys(
295     void* arg, grpc_error_handle error) {
296   AwsExternalAccountCredentials* self =
297       static_cast<AwsExternalAccountCredentials*>(arg);
298   self->OnRetrieveSigningKeysInternal(GRPC_ERROR_REF(error));
299 }
300 
OnRetrieveSigningKeysInternal(grpc_error_handle error)301 void AwsExternalAccountCredentials::OnRetrieveSigningKeysInternal(
302     grpc_error_handle error) {
303   if (error != GRPC_ERROR_NONE) {
304     FinishRetrieveSubjectToken("", error);
305     return;
306   }
307   absl::string_view response_body(ctx_->response.body,
308                                   ctx_->response.body_length);
309   Json json = Json::Parse(response_body, &error);
310   if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
311     FinishRetrieveSubjectToken(
312         "", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
313                 "Invalid retrieve signing keys response.", &error, 1));
314     GRPC_ERROR_UNREF(error);
315     return;
316   }
317   auto it = json.object_value().find("AccessKeyId");
318   if (it != json.object_value().end() &&
319       it->second.type() == Json::Type::STRING) {
320     access_key_id_ = it->second.string_value();
321   } else {
322     FinishRetrieveSubjectToken(
323         "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
324                 absl::StrFormat("Missing or invalid AccessKeyId in %s.",
325                                 response_body)
326                     .c_str()));
327     return;
328   }
329   it = json.object_value().find("SecretAccessKey");
330   if (it != json.object_value().end() &&
331       it->second.type() == Json::Type::STRING) {
332     secret_access_key_ = it->second.string_value();
333   } else {
334     FinishRetrieveSubjectToken(
335         "", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
336                 absl::StrFormat("Missing or invalid SecretAccessKey in %s.",
337                                 response_body)
338                     .c_str()));
339     return;
340   }
341   it = json.object_value().find("Token");
342   if (it != json.object_value().end() &&
343       it->second.type() == Json::Type::STRING) {
344     token_ = it->second.string_value();
345   } else {
346     FinishRetrieveSubjectToken(
347         "",
348         GRPC_ERROR_CREATE_FROM_COPIED_STRING(
349             absl::StrFormat("Missing or invalid Token in %s.", response_body)
350                 .c_str()));
351     return;
352   }
353   BuildSubjectToken();
354 }
355 
BuildSubjectToken()356 void AwsExternalAccountCredentials::BuildSubjectToken() {
357   grpc_error_handle error = GRPC_ERROR_NONE;
358   if (signer_ == nullptr) {
359     cred_verification_url_ = absl::StrReplaceAll(
360         regional_cred_verification_url_, {{"{region}", region_}});
361     signer_ = absl::make_unique<AwsRequestSigner>(
362         access_key_id_, secret_access_key_, token_, "POST",
363         cred_verification_url_, region_, "",
364         std::map<std::string, std::string>(), &error);
365     if (error != GRPC_ERROR_NONE) {
366       FinishRetrieveSubjectToken(
367           "", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
368                   "Creating aws request signer failed.", &error, 1));
369       GRPC_ERROR_UNREF(error);
370       return;
371     }
372   }
373   auto signed_headers = signer_->GetSignedRequestHeaders();
374   if (error != GRPC_ERROR_NONE) {
375     FinishRetrieveSubjectToken("",
376                                GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
377                                    "Invalid getting signed request"
378                                    "headers.",
379                                    &error, 1));
380     GRPC_ERROR_UNREF(error);
381     return;
382   }
383   // Construct subject token
384   Json::Array headers;
385   headers.push_back(Json(
386       {{"key", "Authorization"}, {"value", signed_headers["Authorization"]}}));
387   headers.push_back(Json({{"key", "host"}, {"value", signed_headers["host"]}}));
388   headers.push_back(
389       Json({{"key", "x-amz-date"}, {"value", signed_headers["x-amz-date"]}}));
390   headers.push_back(Json({{"key", "x-amz-security-token"},
391                           {"value", signed_headers["x-amz-security-token"]}}));
392   headers.push_back(
393       Json({{"key", "x-goog-cloud-target-resource"}, {"value", audience_}}));
394   Json::Object object{{"url", Json(cred_verification_url_)},
395                       {"method", Json("POST")},
396                       {"headers", Json(headers)}};
397   Json subject_token_json(object);
398   std::string subject_token = UrlEncode(subject_token_json.Dump());
399   FinishRetrieveSubjectToken(subject_token, GRPC_ERROR_NONE);
400 }
401 
FinishRetrieveSubjectToken(std::string subject_token,grpc_error_handle error)402 void AwsExternalAccountCredentials::FinishRetrieveSubjectToken(
403     std::string subject_token, grpc_error_handle error) {
404   // Reset context
405   ctx_ = nullptr;
406   // Move object state into local variables.
407   auto cb = cb_;
408   cb_ = nullptr;
409   // Invoke the callback.
410   if (error != GRPC_ERROR_NONE) {
411     cb("", error);
412   } else {
413     cb(subject_token, GRPC_ERROR_NONE);
414   }
415 }
416 
417 }  // namespace grpc_core
418