blob: c479bf067fcba3b9c7679c9ded729cc2773b4d3a [file] [log] [blame]
//
// Copyright 2020 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.
//
#include <grpc/support/port_platform.h>
#include "src/core/lib/security/credentials/external/aws_external_account_credentials.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_replace.h"
#include "src/core/lib/gpr/env.h"
namespace grpc_core {
namespace {
const char* kExpectedEnvironmentId = "aws1";
const char* kRegionEnvVar = "AWS_REGION";
const char* kDefaultRegionEnvVar = "AWS_DEFAULT_REGION";
const char* kAccessKeyIdEnvVar = "AWS_ACCESS_KEY_ID";
const char* kSecretAccessKeyEnvVar = "AWS_SECRET_ACCESS_KEY";
const char* kSessionTokenEnvVar = "AWS_SESSION_TOKEN";
std::string UrlEncode(const absl::string_view& s) {
const char* hex = "0123456789ABCDEF";
std::string result;
result.reserve(s.length());
for (auto c : s) {
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') || c == '-' || c == '_' || c == '!' ||
c == '\'' || c == '(' || c == ')' || c == '*' || c == '~' || c == '.') {
result.push_back(c);
} else {
result.push_back('%');
result.push_back(hex[static_cast<unsigned char>(c) >> 4]);
result.push_back(hex[static_cast<unsigned char>(c) & 15]);
}
}
return result;
}
} // namespace
RefCountedPtr<AwsExternalAccountCredentials>
AwsExternalAccountCredentials::Create(Options options,
std::vector<std::string> scopes,
grpc_error_handle* error) {
auto creds = MakeRefCounted<AwsExternalAccountCredentials>(
std::move(options), std::move(scopes), error);
if (*error == GRPC_ERROR_NONE) {
return creds;
} else {
return nullptr;
}
}
AwsExternalAccountCredentials::AwsExternalAccountCredentials(
Options options, std::vector<std::string> scopes, grpc_error_handle* error)
: ExternalAccountCredentials(options, std::move(scopes)) {
audience_ = options.audience;
auto it = options.credential_source.object_value().find("environment_id");
if (it == options.credential_source.object_value().end()) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"environment_id field not present.");
return;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"environment_id field must be a string.");
return;
}
if (it->second.string_value() != kExpectedEnvironmentId) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("environment_id does not match.");
return;
}
it = options.credential_source.object_value().find("region_url");
if (it == options.credential_source.object_value().end()) {
*error =
GRPC_ERROR_CREATE_FROM_STATIC_STRING("region_url field not present.");
return;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"region_url field must be a string.");
return;
}
region_url_ = it->second.string_value();
it = options.credential_source.object_value().find("url");
if (it != options.credential_source.object_value().end() &&
it->second.type() == Json::Type::STRING) {
url_ = it->second.string_value();
}
it = options.credential_source.object_value().find(
"regional_cred_verification_url");
if (it == options.credential_source.object_value().end()) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"regional_cred_verification_url field not present.");
return;
}
if (it->second.type() != Json::Type::STRING) {
*error = GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"regional_cred_verification_url field must be a string.");
return;
}
regional_cred_verification_url_ = it->second.string_value();
}
void AwsExternalAccountCredentials::RetrieveSubjectToken(
HTTPRequestContext* ctx, const Options& /*options*/,
std::function<void(std::string, grpc_error_handle)> cb) {
if (ctx == nullptr) {
FinishRetrieveSubjectToken(
"",
GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Missing HTTPRequestContext to start subject token retrieval."));
return;
}
ctx_ = ctx;
cb_ = cb;
if (signer_ != nullptr) {
BuildSubjectToken();
} else {
RetrieveRegion();
}
}
void AwsExternalAccountCredentials::RetrieveRegion() {
UniquePtr<char> region_from_env(gpr_getenv(kRegionEnvVar));
if (region_from_env == nullptr) {
region_from_env = UniquePtr<char>(gpr_getenv(kDefaultRegionEnvVar));
}
if (region_from_env != nullptr) {
region_ = std::string(region_from_env.get());
if (url_.empty()) {
RetrieveSigningKeys();
} else {
RetrieveRoleName();
}
return;
}
absl::StatusOr<URI> uri = URI::Parse(region_url_);
if (!uri.ok()) {
FinishRetrieveSubjectToken("", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Invalid region url. %s",
uri.status().ToString())
.c_str()));
return;
}
grpc_httpcli_request request;
memset(&request, 0, sizeof(grpc_httpcli_request));
request.host = const_cast<char*>(uri->authority().c_str());
request.http.path = gpr_strdup(uri->path().c_str());
request.handshaker =
uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
grpc_resource_quota* resource_quota =
grpc_resource_quota_create("external_account_credentials");
grpc_http_response_destroy(&ctx_->response);
ctx_->response = {};
GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRegion, this, nullptr);
grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
&request, ctx_->deadline, &ctx_->closure, &ctx_->response);
grpc_resource_quota_unref_internal(resource_quota);
grpc_http_request_destroy(&request.http);
}
void AwsExternalAccountCredentials::OnRetrieveRegion(void* arg,
grpc_error_handle error) {
AwsExternalAccountCredentials* self =
static_cast<AwsExternalAccountCredentials*>(arg);
self->OnRetrieveRegionInternal(GRPC_ERROR_REF(error));
}
void AwsExternalAccountCredentials::OnRetrieveRegionInternal(
grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishRetrieveSubjectToken("", error);
return;
}
// Remove the last letter of availability zone to get pure region
absl::string_view response_body(ctx_->response.body,
ctx_->response.body_length);
region_ = std::string(response_body.substr(0, response_body.size() - 1));
if (url_.empty()) {
RetrieveSigningKeys();
} else {
RetrieveRoleName();
}
}
void AwsExternalAccountCredentials::RetrieveRoleName() {
absl::StatusOr<URI> uri = URI::Parse(url_);
if (!uri.ok()) {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Invalid url: %s.", uri.status().ToString())
.c_str()));
return;
}
grpc_httpcli_request request;
memset(&request, 0, sizeof(grpc_httpcli_request));
request.host = const_cast<char*>(uri->authority().c_str());
request.http.path = gpr_strdup(uri->path().c_str());
request.handshaker =
uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
grpc_resource_quota* resource_quota =
grpc_resource_quota_create("external_account_credentials");
grpc_http_response_destroy(&ctx_->response);
ctx_->response = {};
GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveRoleName, this, nullptr);
grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
&request, ctx_->deadline, &ctx_->closure, &ctx_->response);
grpc_resource_quota_unref_internal(resource_quota);
grpc_http_request_destroy(&request.http);
}
void AwsExternalAccountCredentials::OnRetrieveRoleName(
void* arg, grpc_error_handle error) {
AwsExternalAccountCredentials* self =
static_cast<AwsExternalAccountCredentials*>(arg);
self->OnRetrieveRoleNameInternal(GRPC_ERROR_REF(error));
}
void AwsExternalAccountCredentials::OnRetrieveRoleNameInternal(
grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishRetrieveSubjectToken("", error);
return;
}
role_name_ = std::string(ctx_->response.body, ctx_->response.body_length);
RetrieveSigningKeys();
}
void AwsExternalAccountCredentials::RetrieveSigningKeys() {
UniquePtr<char> access_key_id_from_env(gpr_getenv(kAccessKeyIdEnvVar));
UniquePtr<char> secret_access_key_from_env(
gpr_getenv(kSecretAccessKeyEnvVar));
UniquePtr<char> token_from_env(gpr_getenv(kSessionTokenEnvVar));
if (access_key_id_from_env != nullptr &&
secret_access_key_from_env != nullptr && token_from_env != nullptr) {
access_key_id_ = std::string(access_key_id_from_env.get());
secret_access_key_ = std::string(secret_access_key_from_env.get());
token_ = std::string(token_from_env.get());
BuildSubjectToken();
return;
}
if (role_name_.empty()) {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_FROM_STATIC_STRING(
"Missing role name when retrieving signing keys."));
return;
}
std::string url_with_role_name = absl::StrCat(url_, "/", role_name_);
absl::StatusOr<URI> uri = URI::Parse(url_with_role_name);
if (!uri.ok()) {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Invalid url with role name: %s.",
uri.status().ToString())
.c_str()));
return;
}
grpc_httpcli_request request;
memset(&request, 0, sizeof(grpc_httpcli_request));
request.host = const_cast<char*>(uri->authority().c_str());
request.http.path = gpr_strdup(uri->path().c_str());
request.handshaker =
uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext;
grpc_resource_quota* resource_quota =
grpc_resource_quota_create("external_account_credentials");
grpc_http_response_destroy(&ctx_->response);
ctx_->response = {};
GRPC_CLOSURE_INIT(&ctx_->closure, OnRetrieveSigningKeys, this, nullptr);
grpc_httpcli_get(ctx_->httpcli_context, ctx_->pollent, resource_quota,
&request, ctx_->deadline, &ctx_->closure, &ctx_->response);
grpc_resource_quota_unref_internal(resource_quota);
grpc_http_request_destroy(&request.http);
}
void AwsExternalAccountCredentials::OnRetrieveSigningKeys(
void* arg, grpc_error_handle error) {
AwsExternalAccountCredentials* self =
static_cast<AwsExternalAccountCredentials*>(arg);
self->OnRetrieveSigningKeysInternal(GRPC_ERROR_REF(error));
}
void AwsExternalAccountCredentials::OnRetrieveSigningKeysInternal(
grpc_error_handle error) {
if (error != GRPC_ERROR_NONE) {
FinishRetrieveSubjectToken("", error);
return;
}
absl::string_view response_body(ctx_->response.body,
ctx_->response.body_length);
Json json = Json::Parse(response_body, &error);
if (error != GRPC_ERROR_NONE || json.type() != Json::Type::OBJECT) {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
"Invalid retrieve signing keys response.", &error, 1));
GRPC_ERROR_UNREF(error);
return;
}
auto it = json.object_value().find("AccessKeyId");
if (it != json.object_value().end() &&
it->second.type() == Json::Type::STRING) {
access_key_id_ = it->second.string_value();
} else {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid AccessKeyId in %s.",
response_body)
.c_str()));
return;
}
it = json.object_value().find("SecretAccessKey");
if (it != json.object_value().end() &&
it->second.type() == Json::Type::STRING) {
secret_access_key_ = it->second.string_value();
} else {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid SecretAccessKey in %s.",
response_body)
.c_str()));
return;
}
it = json.object_value().find("Token");
if (it != json.object_value().end() &&
it->second.type() == Json::Type::STRING) {
token_ = it->second.string_value();
} else {
FinishRetrieveSubjectToken(
"",
GRPC_ERROR_CREATE_FROM_COPIED_STRING(
absl::StrFormat("Missing or invalid Token in %s.", response_body)
.c_str()));
return;
}
BuildSubjectToken();
}
void AwsExternalAccountCredentials::BuildSubjectToken() {
grpc_error_handle error = GRPC_ERROR_NONE;
if (signer_ == nullptr) {
cred_verification_url_ = absl::StrReplaceAll(
regional_cred_verification_url_, {{"{region}", region_}});
signer_ = absl::make_unique<AwsRequestSigner>(
access_key_id_, secret_access_key_, token_, "POST",
cred_verification_url_, region_, "",
std::map<std::string, std::string>(), &error);
if (error != GRPC_ERROR_NONE) {
FinishRetrieveSubjectToken(
"", GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
"Creating aws request signer failed.", &error, 1));
GRPC_ERROR_UNREF(error);
return;
}
}
auto signed_headers = signer_->GetSignedRequestHeaders();
if (error != GRPC_ERROR_NONE) {
FinishRetrieveSubjectToken("",
GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
"Invalid getting signed request"
"headers.",
&error, 1));
GRPC_ERROR_UNREF(error);
return;
}
// Construct subject token
Json::Array headers;
headers.push_back(Json(
{{"key", "Authorization"}, {"value", signed_headers["Authorization"]}}));
headers.push_back(Json({{"key", "host"}, {"value", signed_headers["host"]}}));
headers.push_back(
Json({{"key", "x-amz-date"}, {"value", signed_headers["x-amz-date"]}}));
headers.push_back(Json({{"key", "x-amz-security-token"},
{"value", signed_headers["x-amz-security-token"]}}));
headers.push_back(
Json({{"key", "x-goog-cloud-target-resource"}, {"value", audience_}}));
Json::Object object{{"url", Json(cred_verification_url_)},
{"method", Json("POST")},
{"headers", Json(headers)}};
Json subject_token_json(object);
std::string subject_token = UrlEncode(subject_token_json.Dump());
FinishRetrieveSubjectToken(subject_token, GRPC_ERROR_NONE);
}
void AwsExternalAccountCredentials::FinishRetrieveSubjectToken(
std::string subject_token, grpc_error_handle error) {
// Reset context
ctx_ = nullptr;
// Move object state into local variables.
auto cb = cb_;
cb_ = nullptr;
// Invoke the callback.
if (error != GRPC_ERROR_NONE) {
cb("", error);
} else {
cb(subject_token, GRPC_ERROR_NONE);
}
}
} // namespace grpc_core