From d87eab2cee7d8f693f319473fe2c46fc4d51467a Mon Sep 17 00:00:00 2001 From: Carlos O'Ryan Date: Tue, 13 Dec 2022 01:18:04 +0000 Subject: [PATCH 1/2] feat: support external accounts in `GoogleDefaultCredentials()` --- .../internal/oauth2_google_credentials.cc | 9 ++ .../oauth2_google_credentials_test.cc | 148 ++++++++++++------ .../internal/unified_rest_credentials_test.cc | 61 ++++++++ 3 files changed, 171 insertions(+), 47 deletions(-) diff --git a/google/cloud/internal/oauth2_google_credentials.cc b/google/cloud/internal/oauth2_google_credentials.cc index 0a310aadd93ba..9f49e9cc44d8c 100644 --- a/google/cloud/internal/oauth2_google_credentials.cc +++ b/google/cloud/internal/oauth2_google_credentials.cc @@ -18,6 +18,7 @@ #include "google/cloud/internal/oauth2_authorized_user_credentials.h" #include "google/cloud/internal/oauth2_compute_engine_credentials.h" #include "google/cloud/internal/oauth2_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_http_client_factory.h" #include "google/cloud/internal/oauth2_service_account_credentials.h" @@ -79,6 +80,14 @@ StatusOr> LoadCredsFromPath( absl::make_unique( *info, options, std::move(client_factory))); } + if (cred_type == "external_account") { + auto info = + ParseExternalAccountConfiguration(contents, internal::ErrorContext{}); + if (!info) return std::move(info).status(); + return std::unique_ptr( + absl::make_unique( + *std::move(info), std::move(client_factory), options)); + } if (cred_type == "service_account") { auto info = ParseServiceAccountCredentials(contents, path); if (!info) return std::move(info).status(); diff --git a/google/cloud/internal/oauth2_google_credentials_test.cc b/google/cloud/internal/oauth2_google_credentials_test.cc index 0faa69d14192e..f55cb01f56cc6 100644 --- a/google/cloud/internal/oauth2_google_credentials_test.cc +++ b/google/cloud/internal/oauth2_google_credentials_test.cc @@ -18,12 +18,15 @@ #include "google/cloud/internal/oauth2_anonymous_credentials.h" #include "google/cloud/internal/oauth2_authorized_user_credentials.h" #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_service_account_credentials.h" +#include "google/cloud/internal/random.h" #include "google/cloud/testing_util/mock_rest_client.h" #include "google/cloud/testing_util/scoped_environment.h" #include "google/cloud/testing_util/status_matchers.h" #include +#include #include namespace google { @@ -39,6 +42,7 @@ using ::testing::AllOf; using ::testing::HasSubstr; using ::testing::Not; using ::testing::NotNull; +using ::testing::WhenDynamicCastTo; using MockHttpClientFactory = ::testing::MockFunction( @@ -59,7 +63,6 @@ class GoogleCredentialsTest : public ::testing::Test { ScopedEnvironment gcloud_path_override_env_var_; }; -auto constexpr kAuthorizedUserCredFilename = "oauth2-authorized-user.json"; auto constexpr kAuthorizedUserCredContents = R"""({ "client_id": "test-invalid-test-invalid.apps.googleusercontent.com", "client_secret": "invalid-invalid-invalid", @@ -67,6 +70,37 @@ auto constexpr kAuthorizedUserCredContents = R"""({ "type": "authorized_user" })"""; +auto constexpr kServiceAccountCredContents = R"""({ + "type": "service_account", + "project_id": "foo-project", + "private_key_id": "a1a111aa1111a11a11a11aa111a111a1a1111111", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCltiF2oP3KJJ+S\ntTc1McylY+TuAi3AdohX7mmqIjd8a3eBYDHs7FlnUrFC4CRijCr0rUqYfg2pmk4a\n6TaKbQRAhWDJ7XD931g7EBvCtd8+JQBNWVKnP9ByJUaO0hWVniM50KTsWtyX3up/\nfS0W2R8Cyx4yvasE8QHH8gnNGtr94iiORDC7De2BwHi/iU8FxMVJAIyDLNfyk0hN\neheYKfIDBgJV2v6VaCOGWaZyEuD0FJ6wFeLybFBwibrLIBE5Y/StCrZoVZ5LocFP\nT4o8kT7bU6yonudSCyNMedYmqHj/iF8B2UN1WrYx8zvoDqZk0nxIglmEYKn/6U7U\ngyETGcW9AgMBAAECggEAC231vmkpwA7JG9UYbviVmSW79UecsLzsOAZnbtbn1VLT\nPg7sup7tprD/LXHoyIxK7S/jqINvPU65iuUhgCg3Rhz8+UiBhd0pCH/arlIdiPuD\n2xHpX8RIxAq6pGCsoPJ0kwkHSw8UTnxPV8ZCPSRyHV71oQHQgSl/WjNhRi6PQroB\nSqc/pS1m09cTwyKQIopBBVayRzmI2BtBxyhQp9I8t5b7PYkEZDQlbdq0j5Xipoov\n9EW0+Zvkh1FGNig8IJ9Wp+SZi3rd7KLpkyKPY7BK/g0nXBkDxn019cET0SdJOHQG\nDiHiv4yTRsDCHZhtEbAMKZEpku4WxtQ+JjR31l8ueQKBgQDkO2oC8gi6vQDcx/CX\nZ23x2ZUyar6i0BQ8eJFAEN+IiUapEeCVazuxJSt4RjYfwSa/p117jdZGEWD0GxMC\n+iAXlc5LlrrWs4MWUc0AHTgXna28/vii3ltcsI0AjWMqaybhBTTNbMFa2/fV2OX2\nUimuFyBWbzVc3Zb9KAG4Y7OmJQKBgQC5324IjXPq5oH8UWZTdJPuO2cgRsvKmR/r\n9zl4loRjkS7FiOMfzAgUiXfH9XCnvwXMqJpuMw2PEUjUT+OyWjJONEK4qGFJkbN5\n3ykc7p5V7iPPc7Zxj4mFvJ1xjkcj+i5LY8Me+gL5mGIrJ2j8hbuv7f+PWIauyjnp\nNx/0GVFRuQKBgGNT4D1L7LSokPmFIpYh811wHliE0Fa3TDdNGZnSPhaD9/aYyy78\nLkxYKuT7WY7UVvLN+gdNoVV5NsLGDa4cAV+CWPfYr5PFKGXMT/Wewcy1WOmJ5des\nAgMC6zq0TdYmMBN6WpKUpEnQtbmh3eMnuvADLJWxbH3wCkg+4xDGg2bpAoGAYRNk\nMGtQQzqoYNNSkfus1xuHPMA8508Z8O9pwKU795R3zQs1NAInpjI1sOVrNPD7Ymwc\nW7mmNzZbxycCUL/yzg1VW4P1a6sBBYGbw1SMtWxun4ZbnuvMc2CTCh+43/1l+FHe\nMmt46kq/2rH2jwx5feTbOE6P6PINVNRJh/9BDWECgYEAsCWcH9D3cI/QDeLG1ao7\nrE2NcknP8N783edM07Z/zxWsIsXhBPY3gjHVz2LDl+QHgPWhGML62M0ja/6SsJW3\nYvLLIc82V7eqcVJTZtaFkuht68qu/Jn1ezbzJMJ4YXDYo1+KFi+2CAGR06QILb+I\nlUtj+/nH3HDQjM4ltYfTPUg=\n-----END PRIVATE KEY-----\n", + "client_email": "foo-email@foo-project.iam.gserviceaccount.com", + "client_id": "100000000000000000001", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/foo-email%40foo-project.iam.gserviceaccount.com" +})"""; + +auto constexpr kExternalAccountContents = R"""({ + "type": "external_account", + "audience": "test-audience", + "subject_token_type": "test-subject-token-type", + "token_url": "https://sts.example.com/", + "credential_source": {"url": "https://subject.example.com/"} +})"""; + +std::string TempFileName() { + static auto generator = + google::cloud::internal::DefaultPRNG(std::random_device{}()); + return google::cloud::internal::PathAppend( + ::testing::TempDir(), + ::google::cloud::internal::Sample( + generator, 16, "abcdefghijlkmnopqrstuvwxyz0123456789") + + ".json"); +} + /** * @test Verify `GoogleDefaultCredentials()` loads authorized user credentials. * @@ -78,8 +112,7 @@ auto constexpr kAuthorizedUserCredContents = R"""({ * make this an integration test. */ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaEnvVar) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), kAuthorizedUserCredFilename); + auto const filename = TempFileName(); std::ofstream(filename) << kAuthorizedUserCredContents; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -89,16 +122,14 @@ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaEnvVar) { EXPECT_CALL(client_factory, Call).Times(0); auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); ASSERT_STATUS_OK(creds); - // Need to create a temporary for the pointer because clang-tidy warns about - // using expressions with (potential) side-effects inside typeid(). - auto* ptr = creds->get(); - EXPECT_EQ(typeid(*ptr), typeid(AuthorizedUserCredentials)); + EXPECT_THAT(creds->get(), + WhenDynamicCastTo(NotNull())); } TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaGcloudFile) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), kAuthorizedUserCredFilename); + auto const filename = TempFileName(); std::ofstream(filename) << kAuthorizedUserCredContents; auto const env = ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); @@ -108,9 +139,46 @@ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaGcloudFile) { EXPECT_CALL(client_factory, Call).Times(0); auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); ASSERT_STATUS_OK(creds); - auto* ptr = creds->get(); - EXPECT_EQ(typeid(*ptr), typeid(AuthorizedUserCredentials)); + EXPECT_THAT(creds->get(), + WhenDynamicCastTo(NotNull())); +} + +TEST_F(GoogleCredentialsTest, LoadValidExternalAccountCredentialsViaEnvVar) { + auto const filename = TempFileName(); + std::ofstream(filename) << kExternalAccountContents; + auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); + + // Test that the authorized user 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, + LoadValidExternalAccountCredentialsViaGcloudFile) { + auto const filename = TempFileName(); + std::ofstream(filename) << kExternalAccountContents; + auto const env = + ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); + + // Test that the authorized user credentials are loaded as the default when + // stored in the well known gcloud ADC file path. + 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())); } /** @@ -124,22 +192,8 @@ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaGcloudFile) { * make this an integration test. */ -auto constexpr kServiceAccountCredContents = R"""({ - "type": "service_account", - "project_id": "foo-project", - "private_key_id": "a1a111aa1111a11a11a11aa111a111a1a1111111", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCltiF2oP3KJJ+S\ntTc1McylY+TuAi3AdohX7mmqIjd8a3eBYDHs7FlnUrFC4CRijCr0rUqYfg2pmk4a\n6TaKbQRAhWDJ7XD931g7EBvCtd8+JQBNWVKnP9ByJUaO0hWVniM50KTsWtyX3up/\nfS0W2R8Cyx4yvasE8QHH8gnNGtr94iiORDC7De2BwHi/iU8FxMVJAIyDLNfyk0hN\neheYKfIDBgJV2v6VaCOGWaZyEuD0FJ6wFeLybFBwibrLIBE5Y/StCrZoVZ5LocFP\nT4o8kT7bU6yonudSCyNMedYmqHj/iF8B2UN1WrYx8zvoDqZk0nxIglmEYKn/6U7U\ngyETGcW9AgMBAAECggEAC231vmkpwA7JG9UYbviVmSW79UecsLzsOAZnbtbn1VLT\nPg7sup7tprD/LXHoyIxK7S/jqINvPU65iuUhgCg3Rhz8+UiBhd0pCH/arlIdiPuD\n2xHpX8RIxAq6pGCsoPJ0kwkHSw8UTnxPV8ZCPSRyHV71oQHQgSl/WjNhRi6PQroB\nSqc/pS1m09cTwyKQIopBBVayRzmI2BtBxyhQp9I8t5b7PYkEZDQlbdq0j5Xipoov\n9EW0+Zvkh1FGNig8IJ9Wp+SZi3rd7KLpkyKPY7BK/g0nXBkDxn019cET0SdJOHQG\nDiHiv4yTRsDCHZhtEbAMKZEpku4WxtQ+JjR31l8ueQKBgQDkO2oC8gi6vQDcx/CX\nZ23x2ZUyar6i0BQ8eJFAEN+IiUapEeCVazuxJSt4RjYfwSa/p117jdZGEWD0GxMC\n+iAXlc5LlrrWs4MWUc0AHTgXna28/vii3ltcsI0AjWMqaybhBTTNbMFa2/fV2OX2\nUimuFyBWbzVc3Zb9KAG4Y7OmJQKBgQC5324IjXPq5oH8UWZTdJPuO2cgRsvKmR/r\n9zl4loRjkS7FiOMfzAgUiXfH9XCnvwXMqJpuMw2PEUjUT+OyWjJONEK4qGFJkbN5\n3ykc7p5V7iPPc7Zxj4mFvJ1xjkcj+i5LY8Me+gL5mGIrJ2j8hbuv7f+PWIauyjnp\nNx/0GVFRuQKBgGNT4D1L7LSokPmFIpYh811wHliE0Fa3TDdNGZnSPhaD9/aYyy78\nLkxYKuT7WY7UVvLN+gdNoVV5NsLGDa4cAV+CWPfYr5PFKGXMT/Wewcy1WOmJ5des\nAgMC6zq0TdYmMBN6WpKUpEnQtbmh3eMnuvADLJWxbH3wCkg+4xDGg2bpAoGAYRNk\nMGtQQzqoYNNSkfus1xuHPMA8508Z8O9pwKU795R3zQs1NAInpjI1sOVrNPD7Ymwc\nW7mmNzZbxycCUL/yzg1VW4P1a6sBBYGbw1SMtWxun4ZbnuvMc2CTCh+43/1l+FHe\nMmt46kq/2rH2jwx5feTbOE6P6PINVNRJh/9BDWECgYEAsCWcH9D3cI/QDeLG1ao7\nrE2NcknP8N783edM07Z/zxWsIsXhBPY3gjHVz2LDl+QHgPWhGML62M0ja/6SsJW3\nYvLLIc82V7eqcVJTZtaFkuht68qu/Jn1ezbzJMJ4YXDYo1+KFi+2CAGR06QILb+I\nlUtj+/nH3HDQjM4ltYfTPUg=\n-----END PRIVATE KEY-----\n", - "client_email": "foo-email@foo-project.iam.gserviceaccount.com", - "client_id": "100000000000000000001", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/foo-email%40foo-project.iam.gserviceaccount.com" -})"""; - TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaEnvVar) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), kAuthorizedUserCredFilename); + auto const filename = TempFileName(); std::ofstream(filename) << kServiceAccountCredContents; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -149,16 +203,14 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaEnvVar) { EXPECT_CALL(client_factory, Call).Times(0); auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); ASSERT_STATUS_OK(creds); - // Need to create a temporary for the pointer because clang-tidy warns about - // using expressions with (potential) side-effects inside typeid(). - auto* ptr = creds->get(); - EXPECT_EQ(typeid(*ptr), typeid(ServiceAccountCredentials)); + EXPECT_THAT(creds->get(), + WhenDynamicCastTo(NotNull())); } TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaGcloudFile) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), kAuthorizedUserCredFilename); + auto const filename = TempFileName(); std::ofstream(filename) << kServiceAccountCredContents; auto const env = ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); @@ -169,15 +221,17 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaGcloudFile) { EXPECT_CALL(client_factory, Call).Times(0); auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); ASSERT_STATUS_OK(creds); - auto* ptr = creds->get(); - EXPECT_EQ(typeid(*ptr), typeid(ServiceAccountCredentials)); + 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. - auto const env = ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), ""); + auto const filename = TempFileName(); + auto const env = ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename); // If the ADC flow thinks we're on a GCE instance, it should return // ComputeEngineCredentials. MockHttpClientFactory client_factory; @@ -185,13 +239,12 @@ TEST_F(GoogleCredentialsTest, LoadComputeEngineCredentialsFromADCFlow) { auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); ASSERT_STATUS_OK(creds); - auto* ptr = creds->get(); - EXPECT_EQ(typeid(*ptr), typeid(ComputeEngineCredentials)); + EXPECT_THAT(creds->get(), + WhenDynamicCastTo(NotNull())); } TEST_F(GoogleCredentialsTest, LoadUnknownTypeCredentials) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), "oauth2-unknown-type-credentials.json"); + auto const filename = TempFileName(); std::ofstream(filename) << R"""({"type": "unknown_type"})"""; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -199,14 +252,14 @@ TEST_F(GoogleCredentialsTest, LoadUnknownTypeCredentials) { EXPECT_CALL(client_factory, Call).Times(0); auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); EXPECT_THAT(creds, StatusIs(Not(StatusCode::kOk), AllOf(HasSubstr("Unsupported credential type"), HasSubstr(filename)))); } TEST_F(GoogleCredentialsTest, LoadInvalidCredentials) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), "oauth2-invalid-credentials.json"); + auto const filename = TempFileName(); std::ofstream(filename) << R"""( not-a-json-object-string )"""; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -216,11 +269,11 @@ TEST_F(GoogleCredentialsTest, LoadInvalidCredentials) { GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument, HasSubstr("credentials file " + filename))); + (void)std::remove(filename.c_str()); } TEST_F(GoogleCredentialsTest, LoadInvalidAuthorizedUserCredentialsViaADC) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), "oauth2-invalid-au-credentials.json"); + auto const filename = TempFileName(); std::ofstream(filename) << R"""("type": "authorized_user")"""; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -229,11 +282,11 @@ TEST_F(GoogleCredentialsTest, LoadInvalidAuthorizedUserCredentialsViaADC) { auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument)); + (void)std::remove(filename.c_str()); } TEST_F(GoogleCredentialsTest, LoadInvalidServiceAccountCredentialsViaADC) { - auto const filename = google::cloud::internal::PathAppend( - ::testing::TempDir(), "oauth2-invalid-au-credentials.json"); + auto const filename = TempFileName(); std::ofstream(filename) << R"""("type": "service_account")"""; auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); @@ -242,10 +295,11 @@ TEST_F(GoogleCredentialsTest, LoadInvalidServiceAccountCredentialsViaADC) { auto creds = GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction()); EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument)); + (void)std::remove(filename.c_str()); } TEST_F(GoogleCredentialsTest, MissingCredentialsViaEnvVar) { - char const filename[] = "missing-credentials.json"; + auto const filename = TempFileName(); auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename); MockHttpClientFactory client_factory; @@ -258,7 +312,7 @@ TEST_F(GoogleCredentialsTest, MissingCredentialsViaEnvVar) { } TEST_F(GoogleCredentialsTest, MissingCredentialsViaGcloudFilePath) { - char const filename[] = "missing-credentials.json"; + auto const filename = TempFileName(); // The method to create default credentials should see that no file exists at // this path, then continue trying to load the other credential types, diff --git a/google/cloud/internal/unified_rest_credentials_test.cc b/google/cloud/internal/unified_rest_credentials_test.cc index a51afd6a85820..2ff930d56a072 100644 --- a/google/cloud/internal/unified_rest_credentials_test.cc +++ b/google/cloud/internal/unified_rest_credentials_test.cc @@ -276,6 +276,67 @@ TEST(UnifiedRestCredentialsTest, AdcIsComputeEngine) { StatusIs(StatusCode::kPermissionDenied, "uh-oh - GCE token")); } +TEST(UnifiedRestCredentialsTest, AdcIsExternalAccount) { + // This sets up a mocked request for the subject token. + auto const subject_url = std::string{"https://test-only-oidc.example.com/"}; + auto const subject_token = std::string{"test-subject-token"}; + auto subject_token_client = [subject_url, subject_token] { + auto expected_sts_request = Property(&RestRequest::path, subject_url); + auto mock = absl::make_unique(); + EXPECT_CALL(*mock, Get(expected_sts_request)) + .WillOnce(Return(ByMove(MakeMockResponse(subject_token)))); + return mock; + }(); + + // This sets up a mocked request for the token exchange. + auto const sts_url = std::string{"https://sts.example.com/"}; + auto sts_client = [sts_url, subject_token] { + using FormDataType = std::vector>; + auto expected_sts_request = Property(&RestRequest::path, sts_url); + // Check only one value, there are other test for the full contents. + auto expected_form_data = MatcherCast( + Contains(Pair("subject_token", subject_token))); + auto mock = absl::make_unique(); + EXPECT_CALL(*mock, Post(expected_sts_request, expected_form_data)) + .WillOnce(Return( + Status{StatusCode::kPermissionDenied, "uh-oh - STS exchange"})); + return mock; + }(); + + auto expected_options = ::testing::AllOf( + ::testing::ResultOf( + "has UserProjectOption", + [](Options const& o) { return o.has(); }, true), + ::testing::ResultOf( + "UserProjectOption is `test-user-project`", + [](Options const& o) { return o.get(); }, + std::string{"test-user-project"})); + + MockClientFactory client_factory; + EXPECT_CALL(client_factory, Call(expected_options)) + .WillOnce(Return(ByMove(std::move(subject_token_client)))) + .WillOnce(Return(ByMove(std::move(sts_client)))); + + auto const json_external_account = nlohmann::json{ + {"type", "external_account"}, + {"audience", "test-audience"}, + {"subject_token_type", "test-subject-token-type"}, + {"token_url", sts_url}, + {"credential_source", nlohmann::json{{"url", subject_url}}}, + }; + auto const filename = TempKeyFileName(); + auto const env = SetUpAdcFile(filename, json_external_account.dump()); + auto config = std::make_shared( + Options{}.set("test-user-project")); + auto credentials = MapCredentials(config, client_factory.AsStdFunction()); + (void)std::remove(filename.c_str()); + + auto const now = std::chrono::system_clock::now(); + auto access_token = credentials->GetToken(now); + EXPECT_THAT(access_token, + StatusIs(StatusCode::kPermissionDenied, "uh-oh - STS exchange")); +} + TEST(UnifiedRestCredentialsTest, AccessToken) { auto const now = std::chrono::system_clock::now(); auto const expiration = now + std::chrono::seconds(1800); From 5a376bebb0f376a5d48f77ca2c394c63e7651fcf Mon Sep 17 00:00:00 2001 From: Carlos O'Ryan Date: Tue, 13 Dec 2022 18:11:07 +0000 Subject: [PATCH 2/2] Address review comments --- .../cloud/internal/oauth2_google_credentials_test.cc | 12 ++++++------ .../cloud/internal/unified_rest_credentials_test.cc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/google/cloud/internal/oauth2_google_credentials_test.cc b/google/cloud/internal/oauth2_google_credentials_test.cc index f55cb01f56cc6..dccb3158bfc54 100644 --- a/google/cloud/internal/oauth2_google_credentials_test.cc +++ b/google/cloud/internal/oauth2_google_credentials_test.cc @@ -117,7 +117,7 @@ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaEnvVar) { auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); // Test that the authorized user credentials are loaded as the default when - // specified via the well known environment variable. + // specified via the well-known environment variable. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = @@ -134,7 +134,7 @@ TEST_F(GoogleCredentialsTest, LoadValidAuthorizedUserCredentialsViaGcloudFile) { auto const env = ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); // Test that the authorized user credentials are loaded as the default when - // stored in the well known gcloud ADC file path. + // stored in the well-known gcloud ADC file path. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = @@ -151,7 +151,7 @@ TEST_F(GoogleCredentialsTest, LoadValidExternalAccountCredentialsViaEnvVar) { auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); // Test that the authorized user credentials are loaded as the default when - // specified via the well known environment variable. + // specified via the well-known environment variable. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = @@ -170,7 +170,7 @@ TEST_F(GoogleCredentialsTest, ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); // Test that the authorized user credentials are loaded as the default when - // stored in the well known gcloud ADC file path. + // stored in the well-known gcloud ADC file path. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = @@ -198,7 +198,7 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaEnvVar) { auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str()); // Test that the service account credentials are loaded as the default when - // specified via the well known environment variable. + // specified via the well-known environment variable. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = @@ -216,7 +216,7 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaGcloudFile) { ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str()); // Test that the service account credentials are loaded as the default when - // stored in the well known gcloud ADC file path. + // stored in the well-known gcloud ADC file path. MockHttpClientFactory client_factory; EXPECT_CALL(client_factory, Call).Times(0); auto creds = diff --git a/google/cloud/internal/unified_rest_credentials_test.cc b/google/cloud/internal/unified_rest_credentials_test.cc index 2ff930d56a072..2903dc34b0c62 100644 --- a/google/cloud/internal/unified_rest_credentials_test.cc +++ b/google/cloud/internal/unified_rest_credentials_test.cc @@ -293,7 +293,7 @@ TEST(UnifiedRestCredentialsTest, AdcIsExternalAccount) { auto sts_client = [sts_url, subject_token] { using FormDataType = std::vector>; auto expected_sts_request = Property(&RestRequest::path, sts_url); - // Check only one value, there are other test for the full contents. + // Check only one value. There are other tests for the full contents. auto expected_form_data = MatcherCast( Contains(Pair("subject_token", subject_token))); auto mock = absl::make_unique();