diff --git a/google/cloud/google_cloud_cpp_rest_internal.bzl b/google/cloud/google_cloud_cpp_rest_internal.bzl index 573dff90bbd16..b0d4b33156100 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal.bzl @@ -27,6 +27,7 @@ google_cloud_cpp_rest_internal_hdrs = [ "internal/curl_rest_response.h", "internal/curl_wrappers.h", "internal/external_account_parsing.h", + "internal/external_account_source_format.h", "internal/external_account_token_source_file.h", "internal/http_payload.h", "internal/make_jwt_assertion.h", @@ -65,6 +66,7 @@ google_cloud_cpp_rest_internal_srcs = [ "internal/curl_rest_response.cc", "internal/curl_wrappers.cc", "internal/external_account_parsing.cc", + "internal/external_account_source_format.cc", "internal/external_account_token_source_file.cc", "internal/make_jwt_assertion.cc", "internal/oauth2_access_token_credentials.cc", diff --git a/google/cloud/google_cloud_cpp_rest_internal.cmake b/google/cloud/google_cloud_cpp_rest_internal.cmake index 76ac529203607..5c26b967cc5e8 100644 --- a/google/cloud/google_cloud_cpp_rest_internal.cmake +++ b/google/cloud/google_cloud_cpp_rest_internal.cmake @@ -40,6 +40,8 @@ add_library( internal/curl_wrappers.h internal/external_account_parsing.cc internal/external_account_parsing.h + internal/external_account_source_format.cc + internal/external_account_source_format.h internal/external_account_token_source_file.cc internal/external_account_token_source_file.h internal/http_payload.h @@ -198,6 +200,7 @@ if (BUILD_TESTING) internal/curl_wrappers_locking_enabled_test.cc internal/curl_wrappers_test.cc internal/external_account_parsing_test.cc + internal/external_account_source_format_test.cc internal/external_account_token_source_file_test.cc internal/make_jwt_assertion_test.cc internal/oauth2_access_token_credentials_test.cc diff --git a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl index 2daf93f473f19..b32d3a8d948d8 100644 --- a/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl +++ b/google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl @@ -30,6 +30,7 @@ google_cloud_cpp_rest_internal_unit_tests = [ "internal/curl_wrappers_locking_enabled_test.cc", "internal/curl_wrappers_test.cc", "internal/external_account_parsing_test.cc", + "internal/external_account_source_format_test.cc", "internal/external_account_token_source_file_test.cc", "internal/make_jwt_assertion_test.cc", "internal/oauth2_access_token_credentials_test.cc", diff --git a/google/cloud/internal/external_account_source_format.cc b/google/cloud/internal/external_account_source_format.cc new file mode 100644 index 0000000000000..a9600d3679529 --- /dev/null +++ b/google/cloud/internal/external_account_source_format.cc @@ -0,0 +1,55 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/internal/external_account_source_format.h" +#include "google/cloud/internal/absl_str_cat_quiet.h" +#include "google/cloud/internal/external_account_parsing.h" +#include "google/cloud/internal/make_status.h" + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +StatusOr ParseExternalAccountSourceFormat( + nlohmann::json const& credentials_source, + internal::ErrorContext const& ec) { + auto it = credentials_source.find("format"); + if (it == credentials_source.end()) + return ExternalAccountSourceFormat{"text", {}}; + if (!it->is_object()) { + return InvalidArgumentError( + "invalid type for `format` field in `credentials_source`", + GCP_ERROR_INFO().WithContext(ec)); + } + auto const& format = *it; + auto type = ValidateStringField(format, "type", "credentials_source.format", + "text", ec); + if (!type) return std::move(type).status(); + if (*type == "text") return ExternalAccountSourceFormat{"text", {}}; + if (*type != "json") { + return InvalidArgumentError( + absl::StrCat("invalid file type <", *type, "> in `credentials_source`"), + GCP_ERROR_INFO().WithContext(ec)); + } + auto field = ValidateStringField(format, "subject_token_field_name", + "credentials_source.format", ec); + if (!field) return std::move(field).status(); + return ExternalAccountSourceFormat{*std::move(type), *std::move(field)}; +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/external_account_source_format.h b/google/cloud/internal/external_account_source_format.h new file mode 100644 index 0000000000000..f23189686135b --- /dev/null +++ b/google/cloud/internal/external_account_source_format.h @@ -0,0 +1,56 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H +#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H + +#include "google/cloud/internal/error_metadata.h" +#include "google/cloud/status_or.h" +#include "google/cloud/version.h" +#include +#include + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +/** + * The format for external account subject token sources. + * + * External accounts credentials use [OAuth 2.0 Token Exchange][RFC 8693] to + * convert a "subject token" into an "access token". The latter is used (as one + * would expect) to access GCP services. + * + * Some of these sources can return the subject tokens as plain text data, or as + * a string field in a JSON object. `ParseExternalAccountSourceFormat()` + * validates the external source configuration, and returns this struct when + * the validation is successful. + * + * [RFC 8693]: https://www.rfc-editor.org/rfc/rfc8693.html + */ +struct ExternalAccountSourceFormat { + std::string type; + std::string subject_token_field_name; +}; + +StatusOr ParseExternalAccountSourceFormat( + nlohmann::json const& credentials_source, internal::ErrorContext const& ec); + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google + +#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H diff --git a/google/cloud/internal/external_account_source_format_test.cc b/google/cloud/internal/external_account_source_format_test.cc new file mode 100644 index 0000000000000..1ddfaf8adcf79 --- /dev/null +++ b/google/cloud/internal/external_account_source_format_test.cc @@ -0,0 +1,139 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "google/cloud/internal/external_account_source_format.h" +#include "google/cloud/testing_util/status_matchers.h" +#include + +namespace google { +namespace cloud { +namespace oauth2_internal { +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN + +using ::google::cloud::testing_util::StatusIs; +using ::testing::HasSubstr; +using ::testing::IsSupersetOf; +using ::testing::Pair; + +internal::ErrorContext MakeTestErrorContext() { + return internal::ErrorContext{ + {{"filename", "my-credentials.json"}, {"key", "value"}}}; +} + +TEST(ParseExternalAccountSourceFormat, ValidText) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + {"format", nlohmann::json{{"type", "text"}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + ASSERT_STATUS_OK(format); + EXPECT_EQ(format->type, "text"); +} + +TEST(ParseExternalAccountSourceFormat, ValidJson) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + {"format", nlohmann::json{{"type", "json"}, + {"subject_token_field_name", "fieldName"}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + ASSERT_STATUS_OK(format); + EXPECT_EQ(format->type, "json"); + EXPECT_EQ(format->subject_token_field_name, "fieldName"); +} + +TEST(ParseExternalAccountSourceFormat, MissingIsText) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + ASSERT_STATUS_OK(format); + EXPECT_EQ(format->type, "text"); +} + +TEST(ParseExternalAccountSourceFormat, MissingTypeIsText) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + {"format", nlohmann::json{{"unused", "value"}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + ASSERT_STATUS_OK(format); + EXPECT_EQ(format->type, "text"); +} + +TEST(ParseExternalAccountSourceFormat, InvalidFormatType) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + {"format", {{"type", true}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument, + HasSubstr("invalid type for `type` field"))); + EXPECT_THAT(format.status().error_info().metadata(), + IsSupersetOf({Pair("filename", "my-credentials.json"), + Pair("key", "value")})); +} + +TEST(ParseExternalAccountSourceFormat, UnknownFormatType) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.txt"}, + {"format", {{"type", "neither-json-nor-text"}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + EXPECT_THAT(format, + StatusIs(StatusCode::kInvalidArgument, + HasSubstr("invalid file type "))); + EXPECT_THAT(format.status().error_info().metadata(), + IsSupersetOf({Pair("filename", "my-credentials.json"), + Pair("key", "value")})); +} + +TEST(ParseExternalAccountSourceFormat, MissingFormatSubject) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.json"}, + {"format", {{"type", "json"}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument, + HasSubstr("`subject_token_field_name`"))); + EXPECT_THAT(format.status().error_info().metadata(), + IsSupersetOf({Pair("filename", "my-credentials.json"), + Pair("key", "value")})); +} + +TEST(ParseExternalAccountSourceFormat, InvalidFormatSubject) { + auto const creds = nlohmann::json{ + {"file", "/var/run/token-file.json"}, + {"format", {{"type", "json"}, {"subject_token_field_name", true}}}, + }; + auto const format = + ParseExternalAccountSourceFormat(creds, MakeTestErrorContext()); + EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument, + HasSubstr("`subject_token_field_name`"))); + EXPECT_THAT(format.status().error_info().metadata(), + IsSupersetOf({Pair("filename", "my-credentials.json"), + Pair("key", "value")})); +} + +GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END +} // namespace oauth2_internal +} // namespace cloud +} // namespace google diff --git a/google/cloud/internal/external_account_token_source_file.cc b/google/cloud/internal/external_account_token_source_file.cc index 0b4582f7d0db2..a4ce781d4ad32 100644 --- a/google/cloud/internal/external_account_token_source_file.cc +++ b/google/cloud/internal/external_account_token_source_file.cc @@ -13,8 +13,8 @@ // limitations under the License. #include "google/cloud/internal/external_account_token_source_file.h" -#include "google/cloud/internal/absl_str_cat_quiet.h" #include "google/cloud/internal/external_account_parsing.h" +#include "google/cloud/internal/external_account_source_format.h" #include "google/cloud/internal/make_status.h" #include @@ -64,36 +64,6 @@ StatusOr JsonFileReader( return internal::SubjectToken{it->get()}; } -struct Format { - std::string type; - std::string subject_token_field_name; -}; - -StatusOr ParseFormat(nlohmann::json const& credentials_source, - internal::ErrorContext const& ec) { - auto it = credentials_source.find("format"); - if (it == credentials_source.end()) return Format{"text", {}}; - if (!it->is_object()) { - return InvalidArgumentError( - "invalid type for `format` field in `credentials_source`", - GCP_ERROR_INFO().WithContext(ec)); - } - auto const& format = *it; - auto type = ValidateStringField(format, "type", "credentials_source.format", - "text", ec); - if (!type) return std::move(type).status(); - if (*type == "text") return Format{"text", {}}; - if (*type != "json") { - return InvalidArgumentError( - absl::StrCat("invalid file type <", *type, "> in `credentials_source`"), - GCP_ERROR_INFO().WithContext(ec)); - } - auto field = ValidateStringField(format, "subject_token_field_name", - "credentials_source.format", ec); - if (!field) return std::move(field).status(); - return Format{*std::move(type), *std::move(field)}; -} - } // namespace StatusOr MakeExternalAccountTokenSourceFile( @@ -108,7 +78,7 @@ StatusOr MakeExternalAccountTokenSourceFile( auto context = ec; context.emplace_back("credentials_source.type", "file"); context.emplace_back("credentials_source.file.filename", *file); - auto format = ParseFormat(credentials_source, context); + auto format = ParseExternalAccountSourceFormat(credentials_source, context); if (!format) return std::move(format).status(); if (format->type == "text") { context.emplace_back("credentials_source.file.type", "text"); diff --git a/google/cloud/internal/external_account_token_source_file_test.cc b/google/cloud/internal/external_account_token_source_file_test.cc index f3f86e597be8c..02b4a49bc707d 100644 --- a/google/cloud/internal/external_account_token_source_file_test.cc +++ b/google/cloud/internal/external_account_token_source_file_test.cc @@ -103,90 +103,24 @@ TEST(ExternalAccountTokenSource, InvalidFileField) { Pair("key", "value")})); } -TEST(ExternalAccountTokenSource, InvalidFormat) { - auto const creds = nlohmann::json{ - {"file", "/var/run/token-file.txt"}, - {"format", true}, - }; - auto const source = - MakeExternalAccountTokenSourceFile(creds, MakeTestErrorContext()); - EXPECT_THAT(source, - StatusIs(StatusCode::kInvalidArgument, HasSubstr("`format`"))); - EXPECT_THAT(source.status().error_info().metadata(), - IsSupersetOf({Pair("credentials_source.type", "file"), - Pair("filename", "my-credentials.json"), - Pair("key", "value")})); -} - -TEST(ExternalAccountTokenSource, InvalidFormatType) { - auto const creds = nlohmann::json{ - {"file", "/var/run/token-file.txt"}, - {"format", {{"type", true}}}, - }; - auto const source = - MakeExternalAccountTokenSourceFile(creds, MakeTestErrorContext()); - EXPECT_THAT(source, StatusIs(StatusCode::kInvalidArgument, - HasSubstr("invalid type for `type` field"))); - EXPECT_THAT( - source.status().error_info().metadata(), - IsSupersetOf( - {Pair("credentials_source.type", "file"), - Pair("credentials_source.file.filename", "/var/run/token-file.txt"), - Pair("filename", "my-credentials.json"), Pair("key", "value")})); -} - TEST(ExternalAccountTokenSource, UnknownFormatType) { auto const creds = nlohmann::json{ {"file", "/var/run/token-file.txt"}, {"format", {{"type", "neither-json-nor-text"}}}, }; - auto const source = + auto const format = MakeExternalAccountTokenSourceFile(creds, MakeTestErrorContext()); - EXPECT_THAT(source, + EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument, HasSubstr("invalid file type "))); EXPECT_THAT( - source.status().error_info().metadata(), + format.status().error_info().metadata(), IsSupersetOf( {Pair("credentials_source.type", "file"), Pair("credentials_source.file.filename", "/var/run/token-file.txt"), Pair("filename", "my-credentials.json"), Pair("key", "value")})); } -TEST(ExternalAccountTokenSource, MissingFormatSubject) { - auto const creds = nlohmann::json{ - {"file", "/var/run/token-file.json"}, - {"format", {{"type", "json"}}}, - }; - auto const source = - MakeExternalAccountTokenSourceFile(creds, MakeTestErrorContext()); - EXPECT_THAT(source, StatusIs(StatusCode::kInvalidArgument, - HasSubstr("`subject_token_field_name`"))); - EXPECT_THAT( - source.status().error_info().metadata(), - IsSupersetOf( - {Pair("credentials_source.type", "file"), - Pair("credentials_source.file.filename", "/var/run/token-file.json"), - Pair("filename", "my-credentials.json"), Pair("key", "value")})); -} - -TEST(ExternalAccountTokenSource, InvalidFormatSubject) { - auto const creds = nlohmann::json{ - {"file", "/var/run/token-file.json"}, - {"format", {{"type", "json"}, {"subject_token_field_name", true}}}, - }; - auto const source = - MakeExternalAccountTokenSourceFile(creds, MakeTestErrorContext()); - EXPECT_THAT(source, StatusIs(StatusCode::kInvalidArgument, - HasSubstr("`subject_token_field_name`"))); - EXPECT_THAT( - source.status().error_info().metadata(), - IsSupersetOf( - {Pair("credentials_source.type", "file"), - Pair("credentials_source.file.filename", "/var/run/token-file.json"), - Pair("filename", "my-credentials.json"), Pair("key", "value")})); -} - TEST(ExternalAccountTokenSource, MissingTextFile) { auto const token_filename = MakeRandomFilename(); auto const creds = nlohmann::json{{"file", token_filename}};