Skip to content

Commit 07138e9

Browse files
authored
impl: enable GDCH Service Account credentials for gRPC endpoints (#16428)
1 parent ef57798 commit 07138e9

8 files changed

Lines changed: 385 additions & 6 deletions

.typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ extend-exclude = [
4343
"google/cloud/internal/oauth2_google_credentials_test.cc",
4444
"google/cloud/internal/oauth2_service_account_credentials_test.cc",
4545
"google/cloud/internal/rest_client_integration_test.cc",
46+
"google/cloud/internal/unified_grpc_credentials_test.cc",
4647
"google/cloud/testing_util/credentials_constants.h",
4748
"google/cloud/storage/client_options_test.cc",
4849
"google/cloud/storage/client_sign_policy_document_test.cc",

google/cloud/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ cc_library(
153153
"@googleapis//google/rpc:error_details_cc_proto",
154154
"@googleapis//google/rpc:status_cc_proto",
155155
"@grpc//:grpc++",
156+
"@nlohmann_json//:json",
156157
],
157158
)
158159

@@ -178,6 +179,7 @@ cc_library(
178179
"@googleapis//google/bigtable/admin/v2:admin_cc_grpc",
179180
"@googleapis//google/bigtable/v2:bigtable_cc_grpc",
180181
"@googletest//:gtest_main",
182+
"@nlohmann_json//:json",
181183
],
182184
) for test in google_cloud_cpp_grpc_utils_unit_tests]
183185

google/cloud/config-grpc-utils.cmake.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ include(CMakeFindDependencyMacro)
1717
find_dependency(google_cloud_cpp_googleapis)
1818
find_dependency(google_cloud_cpp_common)
1919
find_dependency(absl)
20+
find_dependency(nlohmann_json)
2021

2122
include("${CMAKE_CURRENT_LIST_DIR}/grpc_utils-targets.cmake")

google/cloud/google_cloud_cpp_grpc_utils.cmake

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ~~~
16+
find_package(nlohmann_json CONFIG REQUIRED)
1617

1718
# the library
1819
add_library(
@@ -121,7 +122,8 @@ target_link_libraries(
121122
google-cloud-cpp::rpc_status_protos
122123
google-cloud-cpp::common
123124
gRPC::grpc++
124-
gRPC::grpc)
125+
gRPC::grpc
126+
nlohmann_json::nlohmann_json)
125127
google_cloud_cpp_add_common_options(google_cloud_cpp_grpc_utils)
126128
target_include_directories(
127129
google_cloud_cpp_grpc_utils PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>
@@ -276,6 +278,7 @@ if (BUILD_TESTING)
276278
# List the unit tests, then setup the targets and dependencies.
277279
set(google_cloud_cpp_grpc_utils_integration_tests
278280
# cmake-format: sort
281+
internal/grpc_gdch_service_account_integration_test.cc
279282
internal/grpc_impersonate_service_account_integration_test.cc)
280283

281284
# Export the list of unit and integration tests so the Bazel BUILD file can

google/cloud/google_cloud_cpp_grpc_utils_integration_tests.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
"""Automatically generated unit tests list - DO NOT EDIT."""
1818

1919
google_cloud_cpp_grpc_utils_integration_tests = [
20+
"internal/grpc_gdch_service_account_integration_test.cc",
2021
"internal/grpc_impersonate_service_account_integration_test.cc",
2122
]
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2026 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/completion_queue.h"
16+
#include "google/cloud/credentials.h"
17+
#include "google/cloud/internal/getenv.h"
18+
#include "google/cloud/internal/unified_grpc_credentials.h"
19+
#include "google/cloud/testing_util/scoped_environment.h"
20+
#include "google/cloud/testing_util/status_matchers.h"
21+
#include <gmock/gmock.h>
22+
#if __has_include(<grpcpp/version_info.h>)
23+
#include <grpcpp/version_info.h>
24+
#endif
25+
#include <fstream>
26+
27+
namespace google {
28+
namespace cloud {
29+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
30+
namespace internal {
31+
namespace {
32+
33+
using ::google::cloud::testing_util::IsOk;
34+
using ::google::cloud::testing_util::ScopedEnvironment;
35+
using ::testing::IsEmpty;
36+
using ::testing::Not;
37+
using ::testing::NotNull;
38+
39+
TEST(GrpcGdchServiceAccountIntegrationTest,
40+
RetrievesBearerTokenFromMemoryInAdhocEnvironment) {
41+
#if !defined(GRPC_CPP_VERSION_MAJOR) || \
42+
(GRPC_CPP_VERSION_MAJOR < 1 || \
43+
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR < 84))
44+
GTEST_SKIP() << "GDCH credentials require gRPC >= 1.84.0";
45+
#endif
46+
std::optional<std::string> key_file_env = GetEnv("GRPC_TEST_GDCH_KEY_FILE");
47+
std::optional<std::string> audience_env = GetEnv("GRPC_TEST_GDCH_AUDIENCE");
48+
if (!key_file_env.has_value() || !audience_env.has_value()) GTEST_SKIP();
49+
50+
std::ifstream is(*key_file_env);
51+
std::string contents = std::string{std::istreambuf_iterator<char>{is}, {}};
52+
ASSERT_THAT(contents, Not(IsEmpty()));
53+
54+
CompletionQueue cq;
55+
56+
std::shared_ptr<Credentials> creds =
57+
MakeGDCHServiceAccountCredentials(contents, *audience_env);
58+
ASSERT_THAT(creds, NotNull());
59+
60+
std::shared_ptr<GrpcAuthenticationStrategy> auth =
61+
CreateAuthenticationStrategy(*creds, cq);
62+
ASSERT_THAT(auth, NotNull());
63+
64+
grpc::ClientContext context;
65+
Status status = auth->ConfigureContext(context);
66+
EXPECT_THAT(status, IsOk());
67+
68+
std::shared_ptr<grpc::Channel> channel =
69+
auth->CreateChannel("localhost:443", grpc::ChannelArguments{});
70+
EXPECT_THAT(channel, NotNull());
71+
}
72+
73+
TEST(GrpcGdchServiceAccountIntegrationTest,
74+
RetrievesBearerTokenFromFileInAdhocEnvironment) {
75+
#if !defined(GRPC_CPP_VERSION_MAJOR) || \
76+
(GRPC_CPP_VERSION_MAJOR < 1 || \
77+
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR < 84))
78+
GTEST_SKIP() << "GDCH credentials require gRPC >= 1.84.0";
79+
#endif
80+
std::optional<std::string> key_file_env = GetEnv("GRPC_TEST_GDCH_KEY_FILE");
81+
std::optional<std::string> audience_env = GetEnv("GRPC_TEST_GDCH_AUDIENCE");
82+
if (!key_file_env.has_value() || !audience_env.has_value()) GTEST_SKIP();
83+
84+
std::ifstream is(*key_file_env);
85+
std::string contents = std::string{std::istreambuf_iterator<char>{is}, {}};
86+
ASSERT_THAT(contents, Not(IsEmpty()));
87+
88+
CompletionQueue cq;
89+
90+
ScopedEnvironment env("GOOGLE_APPLICATION_CREDENTIALS", key_file_env);
91+
std::shared_ptr<Credentials> creds =
92+
MakeGDCHServiceAccountCredentials(*audience_env);
93+
ASSERT_THAT(creds, NotNull());
94+
95+
std::shared_ptr<GrpcAuthenticationStrategy> auth =
96+
CreateAuthenticationStrategy(*creds, cq);
97+
ASSERT_THAT(auth, NotNull());
98+
99+
grpc::ClientContext context;
100+
Status status = auth->ConfigureContext(context);
101+
EXPECT_THAT(status, IsOk());
102+
103+
std::shared_ptr<grpc::Channel> channel =
104+
auth->CreateChannel("localhost:443", grpc::ChannelArguments{});
105+
EXPECT_THAT(channel, NotNull());
106+
}
107+
108+
} // namespace
109+
} // namespace internal
110+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
111+
} // namespace cloud
112+
} // namespace google

google/cloud/internal/unified_grpc_credentials.cc

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
#include "google/cloud/internal/grpc_impersonate_service_account.h"
2323
#include "google/cloud/internal/grpc_service_account_authentication.h"
2424
#include <grpcpp/security/credentials.h>
25+
#if __has_include(<grpcpp/version_info.h>)
26+
#include <grpcpp/version_info.h>
27+
#endif
28+
#include <nlohmann/json.hpp>
2529
#include <fstream>
2630

2731
namespace {
@@ -161,12 +165,76 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
161165
"or Access Token Credentials instead.",
162166
GCP_ERROR_INFO())});
163167
}
164-
void visit(GDCHServiceAccountConfig const&) override {
168+
void visit(GDCHServiceAccountConfig const& cfg) override {
169+
#if defined(GRPC_CPP_VERSION_MAJOR) && \
170+
(GRPC_CPP_VERSION_MAJOR > 1 || \
171+
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR >= 84))
172+
std::string json_contents;
173+
if (cfg.file_path().has_value()) {
174+
std::ifstream is(*cfg.file_path());
175+
if (!is.is_open()) {
176+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
177+
ErrorCredentialsConfig{UnknownError(
178+
"Cannot open credentials file " + *cfg.file_path(),
179+
GCP_ERROR_INFO())});
180+
return;
181+
}
182+
json_contents = std::string{std::istreambuf_iterator<char>{is}, {}};
183+
} else if (!cfg.json_object().empty()) {
184+
json_contents = cfg.json_object();
185+
} else {
186+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
187+
ErrorCredentialsConfig{
188+
InternalError("GDCHServiceAccountConfig has neither "
189+
"json_object nor file_path",
190+
GCP_ERROR_INFO())});
191+
return;
192+
}
193+
194+
std::shared_ptr<grpc::CallCredentials> gdch_creds =
195+
grpc::GDCHServiceAccountCredentials(json_contents, cfg.audience());
196+
if (!gdch_creds) {
197+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
198+
ErrorCredentialsConfig{InternalError(
199+
"Error creating grpc::GDCHServiceAccountCredentials",
200+
GCP_ERROR_INFO())});
201+
return;
202+
}
203+
204+
std::string ca_cert_path;
205+
nlohmann::json j = nlohmann::json::parse(json_contents, nullptr, false);
206+
if (!j.is_discarded() && j.is_object()) {
207+
auto it = j.find("ca_cert_path");
208+
if (it != j.end() && it->is_string()) {
209+
ca_cert_path = it->get<std::string>();
210+
}
211+
}
212+
grpc::SslCredentialsOptions ssl_options;
213+
if (!ca_cert_path.empty()) {
214+
std::ifstream is(ca_cert_path);
215+
if (!is.is_open()) {
216+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
217+
ErrorCredentialsConfig{UnknownError(
218+
"Cannot open CA certificate file " + ca_cert_path,
219+
GCP_ERROR_INFO())});
220+
return;
221+
}
222+
ssl_options.pem_root_certs =
223+
std::string{std::istreambuf_iterator<char>{is.rdbuf()}, {}};
224+
} else {
225+
std::optional<std::string> cainfo = LoadCAInfo(options);
226+
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
227+
}
228+
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
229+
grpc::CompositeChannelCredentials(grpc::SslCredentials(ssl_options),
230+
gdch_creds));
231+
#else
232+
(void)cfg;
165233
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
166-
ErrorCredentialsConfig{
167-
UnimplementedError("GDCHServiceAccountCredentials are not yet "
168-
"supported for gRPC endpoints",
169-
GCP_ERROR_INFO())});
234+
ErrorCredentialsConfig{UnimplementedError(
235+
"GDCHServiceAccountCredentials require gRPC v1.84.0 or greater",
236+
GCP_ERROR_INFO())});
237+
#endif
170238
}
171239

172240
} visitor(std::move(cq), std::move(options));

0 commit comments

Comments
 (0)