diff --git a/src/core/lib/security/credentials/external/external_account_credentials.cc b/src/core/lib/security/credentials/external/external_account_credentials.cc index f3c6a78215d..dbcb1238459 100644 --- a/src/core/lib/security/credentials/external/external_account_credentials.cc +++ b/src/core/lib/security/credentials/external/external_account_credentials.cc @@ -17,9 +17,11 @@ #include "src/core/lib/security/credentials/external/external_account_credentials.h" +#include "absl/strings/match.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" +#include "absl/strings/strip.h" #include "absl/time/clock.h" #include "absl/time/time.h" @@ -59,6 +61,23 @@ std::string UrlEncode(const absl::string_view& s) { return result; } +// Expression to match: +// //iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+ +bool MatchWorkforcePoolAudience(absl::string_view audience) { + // Match "//iam.googleapis.com/locations/" + if (!absl::ConsumePrefix(&audience, "//iam.googleapis.com")) return false; + if (!absl::ConsumePrefix(&audience, "/locations/")) return false; + // Match "[^/]+/workforcePools/" + std::pair workforce_pools_split_result = + absl::StrSplit(audience, absl::MaxSplits("/workforcePools/", 1)); + if (absl::StrContains(workforce_pools_split_result.first, '/')) return false; + // Match "[^/]+/providers/.+" + std::pair providers_split_result = + absl::StrSplit(workforce_pools_split_result.second, + absl::MaxSplits("/providers/", 1)); + return !absl::StrContains(providers_split_result.first, '/'); +} + } // namespace RefCountedPtr ExternalAccountCredentials::Create( @@ -151,6 +170,17 @@ RefCountedPtr ExternalAccountCredentials::Create( if (it != json.object_value().end()) { options.client_secret = it->second.string_value(); } + it = json.object_value().find("workforce_pool_user_project"); + if (it != json.object_value().end()) { + if (MatchWorkforcePoolAudience(options.audience)) { + options.workforce_pool_user_project = it->second.string_value(); + } else { + *error = GRPC_ERROR_CREATE_FROM_STATIC_STRING( + "workforce_pool_user_project should not be set for non-workforce " + "pool credentials"); + return nullptr; + } + } RefCountedPtr creds; if (options.credential_source.object_value().find("environment_id") != options.credential_source.object_value().end()) { @@ -267,25 +297,31 @@ void ExternalAccountCredentials::ExchangeToken( request.handshaker = uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext; std::vector body_parts; - body_parts.push_back(absl::StrFormat("%s=%s", "audience", - UrlEncode(options_.audience).c_str())); + body_parts.push_back( + absl::StrFormat("audience=%s", UrlEncode(options_.audience).c_str())); body_parts.push_back(absl::StrFormat( - "%s=%s", "grant_type", + "grant_type=%s", UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_GRANT_TYPE).c_str())); body_parts.push_back(absl::StrFormat( - "%s=%s", "requested_token_type", + "requested_token_type=%s", UrlEncode(EXTERNAL_ACCOUNT_CREDENTIALS_REQUESTED_TOKEN_TYPE).c_str())); + body_parts.push_back(absl::StrFormat( + "subject_token_type=%s", UrlEncode(options_.subject_token_type).c_str())); body_parts.push_back( - absl::StrFormat("%s=%s", "subject_token_type", - UrlEncode(options_.subject_token_type).c_str())); - body_parts.push_back(absl::StrFormat("%s=%s", "subject_token", - UrlEncode(subject_token).c_str())); + absl::StrFormat("subject_token=%s", UrlEncode(subject_token).c_str())); std::string scope = GOOGLE_CLOUD_PLATFORM_DEFAULT_SCOPE; if (options_.service_account_impersonation_url.empty()) { scope = absl::StrJoin(scopes_, " "); } - body_parts.push_back( - absl::StrFormat("%s=%s", "scope", UrlEncode(scope).c_str())); + body_parts.push_back(absl::StrFormat("scope=%s", UrlEncode(scope).c_str())); + Json::Object addtional_options_json_object; + if (options_.client_id.empty() && options_.client_secret.empty()) { + addtional_options_json_object["userProject"] = + options_.workforce_pool_user_project; + } + Json addtional_options_json(std::move(addtional_options_json_object)); + body_parts.push_back(absl::StrFormat( + "options=%s", UrlEncode(addtional_options_json.Dump()).c_str())); std::string body = absl::StrJoin(body_parts, "&"); grpc_resource_quota* resource_quota = grpc_resource_quota_create("external_account_credentials"); @@ -372,7 +408,7 @@ void ExternalAccountCredentials::ImpersenateServiceAccount() { request.handshaker = uri->scheme() == "https" ? &grpc_httpcli_ssl : &grpc_httpcli_plaintext; std::string scope = absl::StrJoin(scopes_, " "); - std::string body = absl::StrFormat("%s=%s", "scope", scope); + std::string body = absl::StrFormat("scope=%s", scope); grpc_resource_quota* resource_quota = grpc_resource_quota_create("external_account_credentials"); grpc_http_response_destroy(&ctx_->response); diff --git a/src/core/lib/security/credentials/external/external_account_credentials.h b/src/core/lib/security/credentials/external/external_account_credentials.h index 3c000968d69..9d0c1753609 100644 --- a/src/core/lib/security/credentials/external/external_account_credentials.h +++ b/src/core/lib/security/credentials/external/external_account_credentials.h @@ -46,6 +46,7 @@ class ExternalAccountCredentials std::string quota_project_id; std::string client_id; std::string client_secret; + std::string workforce_pool_user_project; }; static RefCountedPtr Create( diff --git a/test/core/security/credentials_test.cc b/test/core/security/credentials_test.cc index 5f1e2f1dacd..19267a2e0dc 100644 --- a/test/core/security/credentials_test.cc +++ b/test/core/security/credentials_test.cc @@ -2165,8 +2165,8 @@ validate_external_account_creds_token_exchage_request_with_url_encode( "3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%" "3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&subject_token_type=" "subject_token_type_!%40%23%24&subject_token=test_subject_token&" - "scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform") == - 0); + "scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&" + "options=%7B%7D") == 0); // Check the rest of the request. GPR_ASSERT(strcmp(request->host, "foo.com:5555") == 0); @@ -2371,6 +2371,7 @@ static void test_external_account_creds_success(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {}); /* Check security level. */ @@ -2409,6 +2410,7 @@ static void test_external_account_creds_success_with_url_encode(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {}); RequestMetadataState* state = @@ -2439,6 +2441,7 @@ test_external_account_creds_success_with_service_account_impersonation(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {"scope_1", "scope_2"}); /* Check security level. */ @@ -2469,6 +2472,7 @@ static void test_external_account_creds_failure_invalid_token_url(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {}); grpc_httpcli_set_override(httpcli_get_should_not_be_called, @@ -2504,6 +2508,7 @@ test_external_account_creds_failure_invalid_service_account_impersonation_url( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {}); grpc_httpcli_set_override(httpcli_get_should_not_be_called, @@ -2540,6 +2545,7 @@ test_external_account_creds_failure_token_exchange_response_missing_access_token "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; TestExternalAccountCredentials creds(options, {}); grpc_httpcli_set_override( @@ -2582,6 +2588,7 @@ static void test_url_external_account_creds_success_format_text(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::UrlExternalAccountCredentials::Create(options, {}, &error); @@ -2620,6 +2627,7 @@ test_url_external_account_creds_success_with_qurey_params_format_text(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::UrlExternalAccountCredentials::Create(options, {}, &error); @@ -2657,6 +2665,7 @@ static void test_url_external_account_creds_success_format_json(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::UrlExternalAccountCredentials::Create(options, {}, &error); @@ -2689,6 +2698,7 @@ test_url_external_account_creds_failure_invalid_credential_source_url(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::UrlExternalAccountCredentials::Create(options, {}, &error); @@ -2725,6 +2735,7 @@ static void test_file_external_account_creds_success_format_text(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::FileExternalAccountCredentials::Create(options, {}, &error); @@ -2775,6 +2786,7 @@ static void test_file_external_account_creds_success_format_json(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::FileExternalAccountCredentials::Create(options, {}, &error); @@ -2811,6 +2823,7 @@ static void test_file_external_account_creds_failure_file_not_found(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::FileExternalAccountCredentials::Create(options, {}, &error); @@ -2861,6 +2874,7 @@ static void test_file_external_account_creds_failure_invalid_json_content( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::FileExternalAccountCredentials::Create(options, {}, &error); @@ -2903,6 +2917,7 @@ static void test_aws_external_account_creds_success(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -2941,6 +2956,7 @@ static void test_aws_external_account_creds_success_path_region_env_keys_url( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -2980,6 +2996,7 @@ test_aws_external_account_creds_success_path_default_region_env_keys_url(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3022,6 +3039,7 @@ test_aws_external_account_creds_success_path_duplicate_region_env_keys_url( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3064,6 +3082,7 @@ static void test_aws_external_account_creds_success_path_region_url_keys_env( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3108,6 +3127,7 @@ static void test_aws_external_account_creds_success_path_region_env_keys_env( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3153,6 +3173,7 @@ test_aws_external_account_creds_success_path_default_region_env_keys_env(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3201,6 +3222,7 @@ test_aws_external_account_creds_success_path_duplicate_region_env_keys_env( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3239,6 +3261,7 @@ static void test_aws_external_account_creds_failure_unmatched_environment_id( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3271,6 +3294,7 @@ static void test_aws_external_account_creds_failure_invalid_region_url(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3312,6 +3336,7 @@ static void test_aws_external_account_creds_failure_invalid_url(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3352,6 +3377,7 @@ static void test_aws_external_account_creds_failure_missing_role_name(void) { "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3395,6 +3421,7 @@ test_aws_external_account_creds_failure_invalid_regional_cred_verification_url( "quota_project_id", // quota_project_id; "client_id", // client_id; "client_secret", // client_secret; + "", // workforce_pool_user_project; }; auto creds = grpc_core::AwsExternalAccountCredentials::Create(options, {}, &error); @@ -3502,6 +3529,50 @@ test_external_account_credentials_create_failure_invalid_options_credential_sour GPR_ASSERT(creds == nullptr); } +static void test_external_account_credentials_create_success_workforce_pool( + void) { + const char* url_options_string = + "{\"type\":\"external_account\",\"audience\":\"//iam.googleapis.com/" + "locations/location/workforcePools/pool/providers/provider\",\"subject_" + "token_type\":\"subject_token_type\",\"service_account_impersonation_" + "url\":\"service_account_impersonation_url\",\"token_url\":\"https://" + "foo.com:5555/token\",\"token_info_url\":\"https://foo.com:5555/" + "token_info\",\"credential_source\":{\"url\":\"https://foo.com:5555/" + "generate_subject_token_format_json\",\"headers\":{\"Metadata-Flavor\":" + "\"Google\"},\"format\":{\"type\":\"json\",\"subject_token_field_name\":" + "\"access_token\"}},\"quota_project_id\":\"quota_" + "project_id\",\"client_id\":\"client_id\",\"client_secret\":\"client_" + "secret\",\"workforce_pool_user_project\":\"workforce_pool_user_" + "project\"}"; + const char* url_scopes_string = "scope1,scope2"; + grpc_call_credentials* url_creds = grpc_external_account_credentials_create( + url_options_string, url_scopes_string); + GPR_ASSERT(url_creds != nullptr); + url_creds->Unref(); +} + +static void +test_external_account_credentials_create_failure_invalid_workforce_pool_audience( + void) { + const char* url_options_string = + "{\"type\":\"external_account\",\"audience\":\"invalid_workforce_pool_" + "audience\",\"subject_" + "token_type\":\"subject_token_type\",\"service_account_impersonation_" + "url\":\"service_account_impersonation_url\",\"token_url\":\"https://" + "foo.com:5555/token\",\"token_info_url\":\"https://foo.com:5555/" + "token_info\",\"credential_source\":{\"url\":\"https://foo.com:5555/" + "generate_subject_token_format_json\",\"headers\":{\"Metadata-Flavor\":" + "\"Google\"},\"format\":{\"type\":\"json\",\"subject_token_field_name\":" + "\"access_token\"}},\"quota_project_id\":\"quota_" + "project_id\",\"client_id\":\"client_id\",\"client_secret\":\"client_" + "secret\",\"workforce_pool_user_project\":\"workforce_pool_user_" + "project\"}"; + const char* url_scopes_string = "scope1,scope2"; + grpc_call_credentials* url_creds = grpc_external_account_credentials_create( + url_options_string, url_scopes_string); + GPR_ASSERT(url_creds == nullptr); +} + int main(int argc, char** argv) { grpc::testing::TestEnvironment env(argc, argv); grpc_init(); @@ -3581,6 +3652,8 @@ int main(int argc, char** argv) { test_external_account_credentials_create_failure_invalid_json_format(); test_external_account_credentials_create_failure_invalid_options_format(); test_external_account_credentials_create_failure_invalid_options_credential_source(); + test_external_account_credentials_create_success_workforce_pool(); + test_external_account_credentials_create_failure_invalid_workforce_pool_audience(); grpc_shutdown(); return 0; }