Skip to content

Commit

Permalink
refactor(common): external account format parser (#10281)
Browse files Browse the repository at this point in the history
I will need this for other subject token sources, including URL-based
ones.
  • Loading branch information
coryan authored Nov 20, 2022
1 parent 7d35ce8 commit a8b2f3c
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 101 deletions.
2 changes: 2 additions & 0 deletions google/cloud/google_cloud_cpp_rest_internal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions google/cloud/google_cloud_cpp_rest_internal.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions google/cloud/internal/external_account_source_format.cc
Original file line number Diff line number Diff line change
@@ -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<ExternalAccountSourceFormat> 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
56 changes: 56 additions & 0 deletions google/cloud/internal/external_account_source_format.h
Original file line number Diff line number Diff line change
@@ -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 <nlohmann/json.hpp>
#include <string>

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<ExternalAccountSourceFormat> 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
139 changes: 139 additions & 0 deletions google/cloud/internal/external_account_source_format_test.cc
Original file line number Diff line number Diff line change
@@ -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 <gmock/gmock.h>

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 <neither-json-nor-text>")));
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
34 changes: 2 additions & 32 deletions google/cloud/internal/external_account_token_source_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fstream>

Expand Down Expand Up @@ -64,36 +64,6 @@ StatusOr<internal::SubjectToken> JsonFileReader(
return internal::SubjectToken{it->get<std::string>()};
}

struct Format {
std::string type;
std::string subject_token_field_name;
};

StatusOr<Format> 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<ExternalAccountTokenSource> MakeExternalAccountTokenSourceFile(
Expand All @@ -108,7 +78,7 @@ StatusOr<ExternalAccountTokenSource> 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");
Expand Down
Loading

0 comments on commit a8b2f3c

Please sign in to comment.