diff --git a/google/cloud/internal/oauth2_google_credentials.cc b/google/cloud/internal/oauth2_google_credentials.cc index 3bd5b45189ec8..796670ea4a311 100644 --- a/google/cloud/internal/oauth2_google_credentials.cc +++ b/google/cloud/internal/oauth2_google_credentials.cc @@ -22,6 +22,7 @@ #include "google/cloud/internal/oauth2_external_account_credentials.h" #include "google/cloud/internal/oauth2_google_application_default_credentials_file.h" #include "google/cloud/internal/oauth2_http_client_factory.h" +#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h" #include "google/cloud/internal/oauth2_service_account_credentials.h" #include "google/cloud/internal/parse_service_account_p12_file.h" #include "google/cloud/internal/throw_delegate.h" @@ -36,6 +37,7 @@ namespace oauth2_internal { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { +// NOLINTNEXTLINE(misc-no-recursion) StatusOr> LoadCredsFromString( std::string const& contents, std::string const& path, Options const& options, HttpClientFactory client_factory) { @@ -65,6 +67,30 @@ StatusOr> LoadCredsFromString( std::make_unique(*info, options, std::move(client_factory))); } + if (cred_type == "impersonated_service_account") { + auto info = ParseImpersonatedServiceAccountCredentials(contents, path); + if (!info) return std::move(info).status(); + auto source_creds = LoadCredsFromString(std::move(info->source_credentials), + path, options, client_factory); + if (!source_creds) return std::move(source_creds).status(); + + auto opts = options; + auto& delegates = opts.lookup(); + for (auto& delegate : info->delegates) { + delegates.push_back(std::move(delegate)); + } + + internal::ImpersonateServiceAccountConfig config( + // The base credentials (GUAC) are used to create the IAM REST Stub. We + // are going to override them by supplying our own IAM REST Stub, + // constructed using `oauth2_internal::Credentials`. + nullptr, std::move(info->service_account), opts); + auto rest_stub = MakeMinimalIamCredentialsRestStub( + *std::move(source_creds), opts, std::move(client_factory)); + return std::unique_ptr( + std::make_unique( + config, std::move(rest_stub))); + } return internal::InvalidArgumentError( "Unsupported credential type (" + cred_type + ") when reading Application Default Credentials file " diff --git a/google/cloud/internal/oauth2_google_credentials_test.cc b/google/cloud/internal/oauth2_google_credentials_test.cc index 0aa5b711b1c7a..0ec96c8f5f6b8 100644 --- a/google/cloud/internal/oauth2_google_credentials_test.cc +++ b/google/cloud/internal/oauth2_google_credentials_test.cc @@ -20,6 +20,7 @@ #include "google/cloud/internal/oauth2_compute_engine_credentials.h" #include "google/cloud/internal/oauth2_external_account_credentials.h" #include "google/cloud/internal/oauth2_google_application_default_credentials_file.h" +#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h" #include "google/cloud/internal/oauth2_service_account_credentials.h" #include "google/cloud/internal/random.h" #include "google/cloud/testing_util/mock_rest_client.h" @@ -91,6 +92,21 @@ auto constexpr kExternalAccountContents = R"""({ "credential_source": {"url": "https://subject.example.com/"} })"""; +auto constexpr kImpersonatedServiceAccountContents = R"""({ + "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa3@developer.gserviceaccount.com:generateAccessToken", + "delegates": [ + "sa1@developer.gserviceaccount.com", + "sa2@developer.gserviceaccount.com" + ], + "source_credentials": { + "client_id": "test-invalid-test-invalid.apps.googleusercontent.com", + "client_secret": "invalid-invalid-invalid", + "refresh_token": "1/test-test-test", + "type": "authorized_user" + }, + "type": "impersonated_service_account" +})"""; + std::string TempFileName() { static auto generator = google::cloud::internal::DefaultPRNG(std::random_device{}()); @@ -227,6 +243,45 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaGcloudFile) { WhenDynamicCastTo(NotNull())); } +TEST_F(GoogleCredentialsTest, + LoadValidImpersonatedServiceAccountCredentialsViaEnvVar) { + auto const filename = TempFileName(); + std::ofstream(filename) << kImpersonatedServiceAccountContents; + auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); + + // Test that the impersonated service account credentials are loaded as the + // default when specified via the well-known environment variable. + MockHttpClientFactory client_factory; + EXPECT_CALL(client_factory, Call).Times(0); + auto creds = + GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); + ASSERT_STATUS_OK(creds); + EXPECT_THAT( + creds->get(), + WhenDynamicCastTo(NotNull())); +} + +TEST_F(GoogleCredentialsTest, + LoadValidImpersonatedServiceAccountCredentialsViaGcloudFile) { + auto const filename = TempFileName(); + std::ofstream(filename) << kImpersonatedServiceAccountContents; + auto const env = + ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); + + // Test that the impersonated service account credentials are loaded as the + // default when specified via the well-known environment variable. + MockHttpClientFactory client_factory; + EXPECT_CALL(client_factory, Call).Times(0); + auto creds = + GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); + ASSERT_STATUS_OK(creds); + EXPECT_THAT( + creds->get(), + WhenDynamicCastTo(NotNull())); +} + TEST_F(GoogleCredentialsTest, LoadComputeEngineCredentialsFromADCFlow) { // Developers may have an ADC file in $HOME/.gcloud, override the default // path to a location that is not going to succeed. @@ -298,6 +353,58 @@ TEST_F(GoogleCredentialsTest, LoadInvalidServiceAccountCredentialsViaADC) { (void)std::remove(filename.c_str()); } +TEST_F(GoogleCredentialsTest, + LoadInvalidImpersonatedServiceAccountCredentials) { + auto const filename = TempFileName(); + std::ofstream(filename) << R"""({"type": "impersonated_service_account"})"""; + auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); + + MockHttpClientFactory client_factory; + EXPECT_CALL(client_factory, Call).Times(0); + auto creds = + GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument)); + (void)std::remove(filename.c_str()); +} + +TEST_F(GoogleCredentialsTest, + LoadImpersonatedServiceAccountCredentialsWithInvalidPathUrl) { + auto const filename = TempFileName(); + std::ofstream(filename) << R"""({ + "service_account_impersonation_url": "invalid-url", + "source_credentials": {}, + "type": "impersonated_service_account" +})"""; + auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); + + MockHttpClientFactory client_factory; + EXPECT_CALL(client_factory, Call).Times(0); + auto creds = + GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument)); + (void)std::remove(filename.c_str()); +} + +TEST_F(GoogleCredentialsTest, + LoadImpersonatedServiceAccountWithInvalidSourceCredentials) { + auto const filename = TempFileName(); + std::ofstream(filename) << R"""({ + "service_account_impersonation_url": "invalid-string", + "source_credentials": { + "type": "user_account" + }, + "type": "impersonated_service_account" +})"""; + auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); + + MockHttpClientFactory client_factory; + EXPECT_CALL(client_factory, Call).Times(0); + auto creds = + GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument)); + (void)std::remove(filename.c_str()); +} + TEST_F(GoogleCredentialsTest, MissingCredentialsViaEnvVar) { auto const filename = TempFileName(); auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename);