Skip to content

Commit 2cef6b6

Browse files
authored
impl(common): file-sourced subject tokens (#10277)
To support external accounts credentials (aka BYOID) we need to support reading the subject token from a file. The file itself can be in either text or JSON format.
1 parent 2e80c50 commit 2cef6b6

7 files changed

+608
-0
lines changed

google/cloud/google_cloud_cpp_rest_internal.bzl

Lines changed: 3 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_token_source_file.h",
3031
"internal/http_payload.h",
3132
"internal/make_jwt_assertion.h",
3233
"internal/oauth2_access_token_credentials.h",
@@ -36,6 +37,7 @@ google_cloud_cpp_rest_internal_hdrs = [
3637
"internal/oauth2_credential_constants.h",
3738
"internal/oauth2_credentials.h",
3839
"internal/oauth2_error_credentials.h",
40+
"internal/oauth2_external_account_token_source.h",
3941
"internal/oauth2_google_application_default_credentials_file.h",
4042
"internal/oauth2_google_credentials.h",
4143
"internal/oauth2_impersonate_service_account_credentials.h",
@@ -63,6 +65,7 @@ google_cloud_cpp_rest_internal_srcs = [
6365
"internal/curl_rest_response.cc",
6466
"internal/curl_wrappers.cc",
6567
"internal/external_account_parsing.cc",
68+
"internal/external_account_token_source_file.cc",
6669
"internal/make_jwt_assertion.cc",
6770
"internal/oauth2_access_token_credentials.cc",
6871
"internal/oauth2_anonymous_credentials.cc",

google/cloud/google_cloud_cpp_rest_internal.cmake

Lines changed: 4 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_token_source_file.cc
44+
internal/external_account_token_source_file.h
4345
internal/http_payload.h
4446
internal/make_jwt_assertion.cc
4547
internal/make_jwt_assertion.h
@@ -56,6 +58,7 @@ add_library(
5658
internal/oauth2_credentials.h
5759
internal/oauth2_error_credentials.cc
5860
internal/oauth2_error_credentials.h
61+
internal/oauth2_external_account_token_source.h
5962
internal/oauth2_google_application_default_credentials_file.cc
6063
internal/oauth2_google_application_default_credentials_file.h
6164
internal/oauth2_google_credentials.cc
@@ -195,6 +198,7 @@ if (BUILD_TESTING)
195198
internal/curl_wrappers_locking_enabled_test.cc
196199
internal/curl_wrappers_test.cc
197200
internal/external_account_parsing_test.cc
201+
internal/external_account_token_source_file_test.cc
198202
internal/make_jwt_assertion_test.cc
199203
internal/oauth2_access_token_credentials_test.cc
200204
internal/oauth2_anonymous_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_token_source_file_test.cc",
3334
"internal/make_jwt_assertion_test.cc",
3435
"internal/oauth2_access_token_credentials_test.cc",
3536
"internal/oauth2_anonymous_credentials_test.cc",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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_token_source_file.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+
#include <fstream>
20+
21+
namespace google {
22+
namespace cloud {
23+
namespace oauth2_internal {
24+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
25+
namespace {
26+
27+
Status BadFile(internal::ErrorContext const& ec) {
28+
return InvalidArgumentError("error reading subject token file",
29+
GCP_ERROR_INFO().WithContext(ec));
30+
}
31+
32+
StatusOr<internal::SubjectToken> TextFileReader(
33+
std::string const& filename, internal::ErrorContext const& ec) {
34+
std::ifstream is(filename);
35+
auto contents = std::string{std::istreambuf_iterator<char>(is.rdbuf()), {}};
36+
if (!is.is_open() || is.bad()) return BadFile(ec);
37+
return internal::SubjectToken{std::move(contents)};
38+
}
39+
40+
StatusOr<internal::SubjectToken> JsonFileReader(
41+
std::string const& filename, std::string const& field_name,
42+
internal::ErrorContext const& ec) {
43+
std::ifstream is(filename);
44+
auto contents = std::string{std::istreambuf_iterator<char>(is.rdbuf()), {}};
45+
if (!is.is_open() || is.bad()) return BadFile(ec);
46+
auto json = nlohmann::json::parse(contents, nullptr, false);
47+
auto error_details = [&](std::string const& msg) {
48+
return msg + " in JSON object loaded from `" + filename +
49+
"`, with subject_token_field `" + field_name + "`";
50+
};
51+
if (!json.is_object()) {
52+
return InvalidArgumentError(error_details("parse error"),
53+
GCP_ERROR_INFO().WithContext(ec));
54+
}
55+
auto it = json.find(field_name);
56+
if (it == json.end()) {
57+
return InvalidArgumentError(error_details("subject token field not found"),
58+
GCP_ERROR_INFO().WithContext(ec));
59+
}
60+
if (!it->is_string()) {
61+
return InvalidArgumentError(error_details("invalid type for token field"),
62+
GCP_ERROR_INFO().WithContext(ec));
63+
}
64+
return internal::SubjectToken{it->get<std::string>()};
65+
}
66+
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+
97+
} // namespace
98+
99+
StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceFile(
100+
nlohmann::json const& credentials_source,
101+
internal::ErrorContext const& ec) {
102+
auto file =
103+
ValidateStringField(credentials_source, "file", "credentials_source", ec);
104+
if (!file) return std::move(file).status();
105+
106+
// Make a copy. Most of the time this function should succeed, and we need the
107+
// copy for the lambda captures.
108+
auto context = ec;
109+
context.emplace_back("credentials_source.type", "file");
110+
context.emplace_back("credentials_source.file.filename", *file);
111+
auto format = ParseFormat(credentials_source, context);
112+
if (!format) return std::move(format).status();
113+
if (format->type == "text") {
114+
context.emplace_back("credentials_source.file.type", "text");
115+
return ExternalAccountTokenSource{
116+
[f = *std::move(file), ec = std::move(context)](Options const&) {
117+
return TextFileReader(f, ec);
118+
}};
119+
}
120+
context.emplace_back("credentials_source.file.type", "json");
121+
context.emplace_back("credentials_source.file.source_token_field_name",
122+
format->subject_token_field_name);
123+
return ExternalAccountTokenSource{[f = *std::move(file),
124+
field = format->subject_token_field_name,
125+
ec = std::move(context)](Options const&) {
126+
return JsonFileReader(f, field, ec);
127+
}};
128+
}
129+
130+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
131+
} // namespace oauth2_internal
132+
} // namespace cloud
133+
} // namespace google
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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_TOKEN_SOURCE_FILE_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_TOKEN_SOURCE_FILE_H
17+
18+
#include "google/cloud/internal/error_metadata.h"
19+
#include "google/cloud/internal/oauth2_external_account_token_source.h"
20+
#include "google/cloud/version.h"
21+
#include <nlohmann/json.hpp>
22+
#include <functional>
23+
24+
namespace google {
25+
namespace cloud {
26+
namespace oauth2_internal {
27+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
28+
29+
/**
30+
* Creates an `ExternalAccountTokenSource` for URL-based credential 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+
* External accounts may obtain the subject tokens from several different
37+
* sources. Tokens may be [file-sourced], meaning the client library needs to
38+
* fetch the subject token from a local file. This function validates the
39+
* configuration for file-sourced subject tokens, and returns (if the validation
40+
* is successful) a functor to fetch the token from the URL.
41+
*
42+
* Note that reading the file may fail after this function returns successfully.
43+
* For example, the file may be deleted, or its contents fail to parse after
44+
* the initial read.
45+
*
46+
* [RFC 8693]: https://www.rfc-editor.org/rfc/rfc8693.html
47+
* [file-sourced]:
48+
* https://google.aip.dev/auth/4117#determining-the-subject-token-in-file-sourced-credentials
49+
*/
50+
StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSourceFile(
51+
nlohmann::json const& credentials_source, internal::ErrorContext const& ec);
52+
53+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
54+
} // namespace oauth2_internal
55+
} // namespace cloud
56+
} // namespace google
57+
58+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_INTERNAL_EXTERNAL_ACCOUNT_TOKEN_SOURCE_FILE_H

0 commit comments

Comments
 (0)