Skip to content

Commit

Permalink
impl(oauth2): building blocks for AWS external accounts
Browse files Browse the repository at this point in the history
As with the other external accounts, we need to build a subject token
for AWS external accounts. Building this subject token requires
obtaining a number of pieces of information. These can be found on
environment variables or via the VM's metadata service. The functions
to get them are complex enough that they deserve their own tests.
  • Loading branch information
coryan committed Dec 14, 2022
1 parent f5e10b4 commit cb3c18f
Show file tree
Hide file tree
Showing 3 changed files with 465 additions and 0 deletions.
108 changes: 108 additions & 0 deletions google/cloud/internal/external_account_token_source_aws.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "google/cloud/internal/external_account_token_source_aws.h"
#include "google/cloud/internal/absl_str_cat_quiet.h"
#include "google/cloud/internal/external_account_source_format.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/internal/json_parsing.h"
#include "google/cloud/internal/make_status.h"
#include "absl/strings/match.h"
Expand Down Expand Up @@ -42,8 +43,27 @@ namespace {
auto constexpr kDefaultUrl =
"http://169.254.169.254/latest/meta-data/iam/security-credentials";

auto constexpr kMetadataTokenTtlHeader = "X-aws-ec2-metadata-token-ttl-seconds";
auto constexpr kDefaultMetadataTokenTtl = std::chrono::seconds(900);
auto constexpr kMetadataTokenHeader = "X-aws-ec2-metadata-token";

using ::google::cloud::internal::InvalidArgumentError;

StatusOr<std::string> GetMetadata(std::string path,
std::string const& session_token,
HttpClientFactory const& client_factory,
Options const& opts) {
auto client = client_factory(opts);
auto request = rest_internal::RestRequest().SetPath(std::move(path));
if (!session_token.empty()) {
request.AddHeader(kMetadataTokenHeader, session_token);
}
auto response = client->Get(request);
if (!response) return std::move(response).status();
if (IsHttpError(**response)) return AsStatus(std::move(**response));
return rest_internal::ReadAll(std::move(**response).ExtractPayload());
}

} // namespace

StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceAws(
Expand Down Expand Up @@ -95,6 +115,94 @@ StatusOr<ExternalAccountTokenSourceAwsInfo> ParseExternalAccountTokenSourceAws(
/*imdsv2_session_token_url=*/*std::move(imdsv2)};
}

StatusOr<std::string> FetchMetadataToken(
ExternalAccountTokenSourceAwsInfo const& info,
HttpClientFactory const& client_factory, Options const& opts,
internal::ErrorContext const& /*ec*/) {
if (info.imdsv2_session_token_url.empty()) return std::string{};
auto client = client_factory(opts);
auto request =
rest_internal::RestRequest()
.SetPath(std::move(info.imdsv2_session_token_url))
.AddHeader(kMetadataTokenTtlHeader,
std::to_string(kDefaultMetadataTokenTtl.count()));
auto response = client->Put(request, {});
if (!response) return std::move(response).status();
if (IsHttpError(**response)) return AsStatus(std::move(**response));
return rest_internal::ReadAll(std::move(**response).ExtractPayload());
}

StatusOr<std::string> FetchRegion(ExternalAccountTokenSourceAwsInfo const& info,
std::string const& metadata_token,
HttpClientFactory const& cf,
Options const& opts,
internal::ErrorContext const& ec) {
for (auto const* name : {"AWS_REGION", "AWS_DEFAULT_REGION"}) {
auto env = internal::GetEnv(name);
if (env.has_value()) return std::move(*env);
}

auto payload = GetMetadata(info.region_url, metadata_token, cf, opts);
if (!payload) return std::move(payload).status();
if (payload->empty()) {
return InvalidArgumentError(
absl::StrCat("invalid (empty) region returned from ", info.region_url),
GCP_ERROR_INFO().WithContext(ec));
}
// The metadata service returns an availability zone, we must remove the last
// character to return the region.
payload->pop_back();
return *std::move(payload);
}

StatusOr<ExternalAccountTokenSourceAwsSecrets> FetchSecrets(
ExternalAccountTokenSourceAwsInfo const& info,
std::string const& metadata_token, HttpClientFactory const& cf,
Options const& opts, internal::ErrorContext const& ec) {
auto access_key_id_env = internal::GetEnv("AWS_ACCESS_KEY_ID");
auto secret_access_key_env = internal::GetEnv("AWS_SECRET_ACCESS_KEY");
auto session_token_env = internal::GetEnv("AWS_SESSION_TOKEN");
if (access_key_id_env.has_value() && secret_access_key_env.has_value()) {
return ExternalAccountTokenSourceAwsSecrets{
/*access_key_id=*/std::move(*access_key_id_env),
/*secret_access_key=*/std::move(*secret_access_key_env),
/*session_token=*/session_token_env.value_or("")};
}

// This code fetches the security credentials from the metadata services in an
// AWS EC2 instance, i.e., a VM. The requests and responses are documented in:
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials
auto role = GetMetadata(info.url, metadata_token, cf, opts);
if (!role) return std::move(role).status();
auto path = info.url;
if (path.back() != '/') path.push_back('/');
path.append(*role);
auto secrets = GetMetadata(path, metadata_token, cf, opts);
if (!secrets) return std::move(secrets).status();
auto json = nlohmann::json::parse(*secrets, nullptr, false);
if (!json.is_object()) {
return InvalidArgumentError(
absl::StrCat("cannot parse AWS security-credentials metadata as JSON"),
GCP_ERROR_INFO()
.WithContext(ec)
.WithMetadata("aws.role", *role)
.WithMetadata("aws.metadata.path", path));
}
auto name = absl::string_view{"aws-security-credentials-response"};
auto access_key_id = ValidateStringField(json, "AccessKeyId", name, ec);
if (!access_key_id) return std::move(access_key_id).status();
auto secret_access_key =
ValidateStringField(json, "SecretAccessKey", name, ec);
if (!secret_access_key) return std::move(secret_access_key).status();
auto session_token = ValidateStringField(json, "Token", name, ec);
if (!session_token) return std::move(session_token).status();

return ExternalAccountTokenSourceAwsSecrets{
/*access_key_id=*/*std::move(access_key_id),
/*secret_access_key=*/*std::move(secret_access_key),
/*session_token=*/*std::move(session_token)};
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
Expand Down
30 changes: 30 additions & 0 deletions google/cloud/internal/external_account_token_source_aws.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,39 @@ struct ExternalAccountTokenSourceAwsInfo {
std::string imdsv2_session_token_url;
};

struct ExternalAccountTokenSourceAwsSecrets {
std::string access_key_id;
std::string secret_access_key;
std::string session_token;
};

StatusOr<ExternalAccountTokenSourceAwsInfo> ParseExternalAccountTokenSourceAws(
nlohmann::json const& credentials_source, internal::ErrorContext const& ec);

/**
* If needed, gets the IMDSv2 metadata session token from the AWS EC2 metadata
* server.
*
* If the configuration does not require IMDSv2 tokens, returns an empty string.
*/
StatusOr<std::string> FetchMetadataToken(
ExternalAccountTokenSourceAwsInfo const& info,
HttpClientFactory const& client_factory, Options const& opts,
internal::ErrorContext const& ec);

/// Obtains the AWS region for IMDSv1 configurations.
StatusOr<std::string> FetchRegion(ExternalAccountTokenSourceAwsInfo const& info,
std::string const& metadata_token,
HttpClientFactory const& cf,
Options const& opts,
internal::ErrorContext const& ec);

/// Obtains the AWS secrets for the default role.
StatusOr<ExternalAccountTokenSourceAwsSecrets> FetchSecrets(
ExternalAccountTokenSourceAwsInfo const& info,
std::string const& metadata_token, HttpClientFactory const& cf,
Options const& opts, internal::ErrorContext const& ec);

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
Expand Down
Loading

0 comments on commit cb3c18f

Please sign in to comment.