Skip to content

Commit a8b2f3c

Browse files
authored
refactor(common): external account format parser (#10281)
I will need this for other subject token sources, including URL-based ones.
1 parent 7d35ce8 commit a8b2f3c

8 files changed

+261
-101
lines changed

google/cloud/google_cloud_cpp_rest_internal.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ google_cloud_cpp_rest_internal_hdrs = [
2727
"internal/curl_rest_response.h",
2828
"internal/curl_wrappers.h",
2929
"internal/external_account_parsing.h",
30+
"internal/external_account_source_format.h",
3031
"internal/external_account_token_source_file.h",
3132
"internal/http_payload.h",
3233
"internal/make_jwt_assertion.h",
@@ -65,6 +66,7 @@ google_cloud_cpp_rest_internal_srcs = [
6566
"internal/curl_rest_response.cc",
6667
"internal/curl_wrappers.cc",
6768
"internal/external_account_parsing.cc",
69+
"internal/external_account_source_format.cc",
6870
"internal/external_account_token_source_file.cc",
6971
"internal/make_jwt_assertion.cc",
7072
"internal/oauth2_access_token_credentials.cc",

google/cloud/google_cloud_cpp_rest_internal.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ add_library(
4040
internal/curl_wrappers.h
4141
internal/external_account_parsing.cc
4242
internal/external_account_parsing.h
43+
internal/external_account_source_format.cc
44+
internal/external_account_source_format.h
4345
internal/external_account_token_source_file.cc
4446
internal/external_account_token_source_file.h
4547
internal/http_payload.h
@@ -198,6 +200,7 @@ if (BUILD_TESTING)
198200
internal/curl_wrappers_locking_enabled_test.cc
199201
internal/curl_wrappers_test.cc
200202
internal/external_account_parsing_test.cc
203+
internal/external_account_source_format_test.cc
201204
internal/external_account_token_source_file_test.cc
202205
internal/make_jwt_assertion_test.cc
203206
internal/oauth2_access_token_credentials_test.cc

google/cloud/google_cloud_cpp_rest_internal_unit_tests.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ google_cloud_cpp_rest_internal_unit_tests = [
3030
"internal/curl_wrappers_locking_enabled_test.cc",
3131
"internal/curl_wrappers_test.cc",
3232
"internal/external_account_parsing_test.cc",
33+
"internal/external_account_source_format_test.cc",
3334
"internal/external_account_token_source_file_test.cc",
3435
"internal/make_jwt_assertion_test.cc",
3536
"internal/oauth2_access_token_credentials_test.cc",
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "google/cloud/internal/external_account_source_format.h"
16+
#include "google/cloud/internal/absl_str_cat_quiet.h"
17+
#include "google/cloud/internal/external_account_parsing.h"
18+
#include "google/cloud/internal/make_status.h"
19+
20+
namespace google {
21+
namespace cloud {
22+
namespace oauth2_internal {
23+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
24+
25+
StatusOr<ExternalAccountSourceFormat> ParseExternalAccountSourceFormat(
26+
nlohmann::json const& credentials_source,
27+
internal::ErrorContext const& ec) {
28+
auto it = credentials_source.find("format");
29+
if (it == credentials_source.end())
30+
return ExternalAccountSourceFormat{"text", {}};
31+
if (!it->is_object()) {
32+
return InvalidArgumentError(
33+
"invalid type for `format` field in `credentials_source`",
34+
GCP_ERROR_INFO().WithContext(ec));
35+
}
36+
auto const& format = *it;
37+
auto type = ValidateStringField(format, "type", "credentials_source.format",
38+
"text", ec);
39+
if (!type) return std::move(type).status();
40+
if (*type == "text") return ExternalAccountSourceFormat{"text", {}};
41+
if (*type != "json") {
42+
return InvalidArgumentError(
43+
absl::StrCat("invalid file type <", *type, "> in `credentials_source`"),
44+
GCP_ERROR_INFO().WithContext(ec));
45+
}
46+
auto field = ValidateStringField(format, "subject_token_field_name",
47+
"credentials_source.format", ec);
48+
if (!field) return std::move(field).status();
49+
return ExternalAccountSourceFormat{*std::move(type), *std::move(field)};
50+
}
51+
52+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
53+
} // namespace oauth2_internal
54+
} // namespace cloud
55+
} // namespace google
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H
17+
18+
#include "google/cloud/internal/error_metadata.h"
19+
#include "google/cloud/status_or.h"
20+
#include "google/cloud/version.h"
21+
#include <nlohmann/json.hpp>
22+
#include <string>
23+
24+
namespace google {
25+
namespace cloud {
26+
namespace oauth2_internal {
27+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
28+
29+
/**
30+
* The format for external account subject token sources.
31+
*
32+
* External accounts credentials use [OAuth 2.0 Token Exchange][RFC 8693] to
33+
* convert a "subject token" into an "access token". The latter is used (as one
34+
* would expect) to access GCP services.
35+
*
36+
* Some of these sources can return the subject tokens as plain text data, or as
37+
* a string field in a JSON object. `ParseExternalAccountSourceFormat()`
38+
* validates the external source configuration, and returns this struct when
39+
* the validation is successful.
40+
*
41+
* [RFC 8693]: https://www.rfc-editor.org/rfc/rfc8693.html
42+
*/
43+
struct ExternalAccountSourceFormat {
44+
std::string type;
45+
std::string subject_token_field_name;
46+
};
47+
48+
StatusOr<ExternalAccountSourceFormat> ParseExternalAccountSourceFormat(
49+
nlohmann::json const& credentials_source, internal::ErrorContext const& ec);
50+
51+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
52+
} // namespace oauth2_internal
53+
} // namespace cloud
54+
} // namespace google
55+
56+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_SOURCE_FORMAT_H
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "google/cloud/internal/external_account_source_format.h"
16+
#include "google/cloud/testing_util/status_matchers.h"
17+
#include <gmock/gmock.h>
18+
19+
namespace google {
20+
namespace cloud {
21+
namespace oauth2_internal {
22+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
23+
24+
using ::google::cloud::testing_util::StatusIs;
25+
using ::testing::HasSubstr;
26+
using ::testing::IsSupersetOf;
27+
using ::testing::Pair;
28+
29+
internal::ErrorContext MakeTestErrorContext() {
30+
return internal::ErrorContext{
31+
{{"filename", "my-credentials.json"}, {"key", "value"}}};
32+
}
33+
34+
TEST(ParseExternalAccountSourceFormat, ValidText) {
35+
auto const creds = nlohmann::json{
36+
{"file", "/var/run/token-file.txt"},
37+
{"format", nlohmann::json{{"type", "text"}}},
38+
};
39+
auto const format =
40+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
41+
ASSERT_STATUS_OK(format);
42+
EXPECT_EQ(format->type, "text");
43+
}
44+
45+
TEST(ParseExternalAccountSourceFormat, ValidJson) {
46+
auto const creds = nlohmann::json{
47+
{"file", "/var/run/token-file.txt"},
48+
{"format", nlohmann::json{{"type", "json"},
49+
{"subject_token_field_name", "fieldName"}}},
50+
};
51+
auto const format =
52+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
53+
ASSERT_STATUS_OK(format);
54+
EXPECT_EQ(format->type, "json");
55+
EXPECT_EQ(format->subject_token_field_name, "fieldName");
56+
}
57+
58+
TEST(ParseExternalAccountSourceFormat, MissingIsText) {
59+
auto const creds = nlohmann::json{
60+
{"file", "/var/run/token-file.txt"},
61+
};
62+
auto const format =
63+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
64+
ASSERT_STATUS_OK(format);
65+
EXPECT_EQ(format->type, "text");
66+
}
67+
68+
TEST(ParseExternalAccountSourceFormat, MissingTypeIsText) {
69+
auto const creds = nlohmann::json{
70+
{"file", "/var/run/token-file.txt"},
71+
{"format", nlohmann::json{{"unused", "value"}}},
72+
};
73+
auto const format =
74+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
75+
ASSERT_STATUS_OK(format);
76+
EXPECT_EQ(format->type, "text");
77+
}
78+
79+
TEST(ParseExternalAccountSourceFormat, InvalidFormatType) {
80+
auto const creds = nlohmann::json{
81+
{"file", "/var/run/token-file.txt"},
82+
{"format", {{"type", true}}},
83+
};
84+
auto const format =
85+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
86+
EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument,
87+
HasSubstr("invalid type for `type` field")));
88+
EXPECT_THAT(format.status().error_info().metadata(),
89+
IsSupersetOf({Pair("filename", "my-credentials.json"),
90+
Pair("key", "value")}));
91+
}
92+
93+
TEST(ParseExternalAccountSourceFormat, UnknownFormatType) {
94+
auto const creds = nlohmann::json{
95+
{"file", "/var/run/token-file.txt"},
96+
{"format", {{"type", "neither-json-nor-text"}}},
97+
};
98+
auto const format =
99+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
100+
EXPECT_THAT(format,
101+
StatusIs(StatusCode::kInvalidArgument,
102+
HasSubstr("invalid file type <neither-json-nor-text>")));
103+
EXPECT_THAT(format.status().error_info().metadata(),
104+
IsSupersetOf({Pair("filename", "my-credentials.json"),
105+
Pair("key", "value")}));
106+
}
107+
108+
TEST(ParseExternalAccountSourceFormat, MissingFormatSubject) {
109+
auto const creds = nlohmann::json{
110+
{"file", "/var/run/token-file.json"},
111+
{"format", {{"type", "json"}}},
112+
};
113+
auto const format =
114+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
115+
EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument,
116+
HasSubstr("`subject_token_field_name`")));
117+
EXPECT_THAT(format.status().error_info().metadata(),
118+
IsSupersetOf({Pair("filename", "my-credentials.json"),
119+
Pair("key", "value")}));
120+
}
121+
122+
TEST(ParseExternalAccountSourceFormat, InvalidFormatSubject) {
123+
auto const creds = nlohmann::json{
124+
{"file", "/var/run/token-file.json"},
125+
{"format", {{"type", "json"}, {"subject_token_field_name", true}}},
126+
};
127+
auto const format =
128+
ParseExternalAccountSourceFormat(creds, MakeTestErrorContext());
129+
EXPECT_THAT(format, StatusIs(StatusCode::kInvalidArgument,
130+
HasSubstr("`subject_token_field_name`")));
131+
EXPECT_THAT(format.status().error_info().metadata(),
132+
IsSupersetOf({Pair("filename", "my-credentials.json"),
133+
Pair("key", "value")}));
134+
}
135+
136+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
137+
} // namespace oauth2_internal
138+
} // namespace cloud
139+
} // namespace google

google/cloud/internal/external_account_token_source_file.cc

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
// limitations under the License.
1414

1515
#include "google/cloud/internal/external_account_token_source_file.h"
16-
#include "google/cloud/internal/absl_str_cat_quiet.h"
1716
#include "google/cloud/internal/external_account_parsing.h"
17+
#include "google/cloud/internal/external_account_source_format.h"
1818
#include "google/cloud/internal/make_status.h"
1919
#include <fstream>
2020

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

67-
struct Format {
68-
std::string type;
69-
std::string subject_token_field_name;
70-
};
71-
72-
StatusOr<Format> ParseFormat(nlohmann::json const& credentials_source,
73-
internal::ErrorContext const& ec) {
74-
auto it = credentials_source.find("format");
75-
if (it == credentials_source.end()) return Format{"text", {}};
76-
if (!it->is_object()) {
77-
return InvalidArgumentError(
78-
"invalid type for `format` field in `credentials_source`",
79-
GCP_ERROR_INFO().WithContext(ec));
80-
}
81-
auto const& format = *it;
82-
auto type = ValidateStringField(format, "type", "credentials_source.format",
83-
"text", ec);
84-
if (!type) return std::move(type).status();
85-
if (*type == "text") return Format{"text", {}};
86-
if (*type != "json") {
87-
return InvalidArgumentError(
88-
absl::StrCat("invalid file type <", *type, "> in `credentials_source`"),
89-
GCP_ERROR_INFO().WithContext(ec));
90-
}
91-
auto field = ValidateStringField(format, "subject_token_field_name",
92-
"credentials_source.format", ec);
93-
if (!field) return std::move(field).status();
94-
return Format{*std::move(type), *std::move(field)};
95-
}
96-
9767
} // namespace
9868

9969
StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceFile(
@@ -108,7 +78,7 @@ StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceFile(
10878
auto context = ec;
10979
context.emplace_back("credentials_source.type", "file");
11080
context.emplace_back("credentials_source.file.filename", *file);
111-
auto format = ParseFormat(credentials_source, context);
81+
auto format = ParseExternalAccountSourceFormat(credentials_source, context);
11282
if (!format) return std::move(format).status();
11383
if (format->type == "text") {
11484
context.emplace_back("credentials_source.file.type", "text");

0 commit comments

Comments
 (0)