Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impl(oauth2): support IMDSv2 in AWS token sources #10434

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion google/cloud/internal/external_account_token_source_aws.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include "google/cloud/internal/external_account_source_format.h"
#include "google/cloud/internal/json_parsing.h"
#include "google/cloud/internal/make_status.h"
#include "google/cloud/internal/rest_request.h"
#include "google/cloud/internal/rest_response.h"
#include "absl/strings/match.h"

namespace google {
Expand Down Expand Up @@ -44,11 +46,34 @@ auto constexpr kDefaultUrl =

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

StatusOr<internal::SubjectToken> Idmsv2TokenSource(
ExternalAccountTokenSourceAwsInfo const& info,
HttpClientFactory const& client_factory, Options const& opts) {
auto client = client_factory(opts);
auto request =
rest_internal::RestRequest().SetPath(info.imdsv2_session_token_url);
auto response = client->Get(request);
if (!response) return std::move(response).status();
if (IsHttpError(**response)) return AsStatus(std::move(**response));
auto payload = rest_internal::ReadAll(std::move(**response).ExtractPayload());
if (!payload) return std::move(payload).status();
return internal::SubjectToken{*std::move(payload)};
}

} // namespace

StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceAws(
nlohmann::json const& /*credentials_source*/,
nlohmann::json const& credentials_source,
internal::ErrorContext const& ec) {
auto info = ParseExternalAccountTokenSourceAws(credentials_source, ec);
if (!info) return std::move(info).status();
if (!info->imdsv2_session_token_url.empty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverse test to make "success" the fall through?

return ExternalAccountTokenSource{
[info = *std::move(info)](HttpClientFactory const& cf,
Options const& opts) {
return Idmsv2TokenSource(info, cf, opts);
}};
}
return internal::UnimplementedError("WIP", GCP_ERROR_INFO().WithContext(ec));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"WIP"?

}

Expand Down
55 changes: 55 additions & 0 deletions google/cloud/internal/external_account_token_source_aws_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,21 @@ namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace {

using ::google::cloud::rest_internal::HttpStatusCode;
using ::google::cloud::rest_internal::RestRequest;
using ::google::cloud::rest_internal::RestResponse;
using ::google::cloud::testing_util::MakeMockHttpPayloadSuccess;
using ::google::cloud::testing_util::MockRestClient;
using ::google::cloud::testing_util::MockRestResponse;
using ::google::cloud::testing_util::StatusIs;
using ::testing::ByMove;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::IsSupersetOf;
using ::testing::Pair;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::Return;

using MockClientFactory =
::testing::MockFunction<std::unique_ptr<rest_internal::RestClient>(
Expand All @@ -41,6 +51,51 @@ internal::ErrorContext MakeTestErrorContext() {
{{"filename", "my-credentials.json"}, {"key", "value"}}};
}

std::unique_ptr<RestResponse> MakeMockResponseSuccess(std::string contents) {
auto response = absl::make_unique<MockRestResponse>();
EXPECT_CALL(*response, StatusCode)
.WillRepeatedly(Return(HttpStatusCode::kOk));
EXPECT_CALL(std::move(*response), ExtractPayload)
.WillOnce(
Return(ByMove(MakeMockHttpPayloadSuccess(std::move(contents)))));
return response;
}

TEST(ExternalAccountTokenSource, WorkingImdsv2SessionTokenUrl) {
auto const test_url = std::string{"http://169.254.169.254/latest/api/token"};
auto const token = std::string{"a-test-only-token"};

auto expected_options =
ResultOf([](Options const& o) { return o.get<QuotaUserOption>(); },
"test-quota-user");

MockClientFactory client_factory;
EXPECT_CALL(client_factory, Call(expected_options))
.WillOnce([test_url, token]() {
auto mock = absl::make_unique<MockRestClient>();
auto expected_request = Property(&RestRequest::path, test_url);
EXPECT_CALL(*mock, Get(expected_request))
.WillOnce(Return(ByMove(MakeMockResponseSuccess(token))));
return mock;
});

auto const creds = nlohmann::json{
{"environment_id", "aws1"},
{"region_url", "test-region-url"},
{"url", "test-url"},
{"regional_cred_verification_url", "test-verification-url"},
{"imdsv2_session_token_url", test_url},
};
auto const source =
MakeExternalAccountTokenSourceAws(creds, MakeTestErrorContext());
ASSERT_STATUS_OK(source);
auto const actual =
(*source)(client_factory.AsStdFunction(),
Options{}.set<QuotaUserOption>("test-quota-user"));
ASSERT_STATUS_OK(actual);
EXPECT_EQ(*actual, internal::SubjectToken{token});
}

TEST(ExternalAccountTokenSource, ParseSuccess) {
auto const creds = nlohmann::json{
{"environment_id", "aws1"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/absl_str_cat_quiet.h"
#include "google/cloud/internal/external_account_token_source_aws.h"
#include "google/cloud/internal/external_account_token_source_file.h"
#include "google/cloud/internal/external_account_token_source_url.h"
#include "google/cloud/internal/json_parsing.h"
Expand All @@ -35,7 +36,9 @@ using ::google::cloud::internal::InvalidArgumentError;
StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSource(
nlohmann::json const& credentials_source,
internal::ErrorContext const& ec) {
auto source = MakeExternalAccountTokenSourceUrl(credentials_source, ec);
auto source = MakeExternalAccountTokenSourceAws(credentials_source, ec);
if (source) return source;
source = MakeExternalAccountTokenSourceUrl(credentials_source, ec);
if (source) return source;
source = MakeExternalAccountTokenSourceFile(credentials_source, ec);
if (source) return source;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,33 @@ struct TestOnlyOption {
using Type = std::string;
};

TEST(ExternalAccount, ParseAwsSuccess) {
auto const configuration = nlohmann::json{
{"type", "external_account"},
{"audience", "test-audience"},
{"subject_token_type", "test-subject-token-type"},
{"token_url", "test-token-url"},
{"credential_source",
nlohmann::json{
{"environment_id", "aws1"},
{"region_url",
"http://169.254.169.254/latest/meta-data/placement/"
"availability-zone"},
{"regional_cred_verification_url", "test-verification-url"},
{"imdsv2_session_token_url",
"http://169.254.169.254/latest/api/token"},
}},
};
auto ec = internal::ErrorContext(
{{"program", "test"}, {"full-configuration", configuration.dump()}});
auto const actual =
ParseExternalAccountConfiguration(configuration.dump(), ec);
ASSERT_STATUS_OK(actual);
EXPECT_EQ(actual->audience, "test-audience");
EXPECT_EQ(actual->subject_token_type, "test-subject-token-type");
EXPECT_EQ(actual->token_url, "test-token-url");
}

TEST(ExternalAccount, ParseUrlSuccess) {
auto const configuration = nlohmann::json{
{"type", "external_account"},
Expand Down Expand Up @@ -420,7 +447,8 @@ TEST(ExternalAccount, ParseMissingCredentialSource) {
{"audience", "test-audience"},
{"subject_token_type", "test-subject-token-type"},
{"token_url", "test-token-url"},
// {"credential_source", nlohmann::json{{"file", "/dev/null-test-only"}}},
// {"credential_source", nlohmann::json{{"file",
// "/dev/null-test-only"}}},
};
auto ec = internal::ErrorContext(
{{"program", "test"}, {"full-configuration", configuration.dump()}});
Expand Down Expand Up @@ -882,7 +910,8 @@ TEST(ExternalAccount, MissingIssuedTokenType) {
auto const json_response = nlohmann::json{
{"access_token", expected_access_token},
{"expires_in", expected_expires_in.count()},
// {"issued_token_type", "urn:ietf:params:oauth:token-type:access_token"},
// {"issued_token_type",
// "urn:ietf:params:oauth:token-type:access_token"},
{"token_type", "Bearer"},
};
auto mock_source = [](HttpClientFactory const&, Options const&) {
Expand Down