Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ extend-exclude = [
"google/cloud/internal/oauth2_google_credentials_test.cc",
"google/cloud/internal/oauth2_service_account_credentials_test.cc",
"google/cloud/internal/rest_client_integration_test.cc",
"google/cloud/internal/unified_grpc_credentials_test.cc",
"google/cloud/testing_util/credentials_constants.h",
"google/cloud/storage/client_options_test.cc",
"google/cloud/storage/client_sign_policy_document_test.cc",
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ cc_library(
"@googleapis//google/rpc:error_details_cc_proto",
"@googleapis//google/rpc:status_cc_proto",
"@grpc//:grpc++",
"@nlohmann_json//:json",
],
)

Expand All @@ -178,6 +179,7 @@ cc_library(
"@googleapis//google/bigtable/admin/v2:admin_cc_grpc",
"@googleapis//google/bigtable/v2:bigtable_cc_grpc",
"@googletest//:gtest_main",
"@nlohmann_json//:json",
],
) for test in google_cloud_cpp_grpc_utils_unit_tests]

Expand Down
1 change: 1 addition & 0 deletions google/cloud/config-grpc-utils.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ include(CMakeFindDependencyMacro)
find_dependency(google_cloud_cpp_googleapis)
find_dependency(google_cloud_cpp_common)
find_dependency(absl)
find_dependency(nlohmann_json)

include("${CMAKE_CURRENT_LIST_DIR}/grpc_utils-targets.cmake")
5 changes: 4 additions & 1 deletion google/cloud/google_cloud_cpp_grpc_utils.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ~~~
find_package(nlohmann_json CONFIG REQUIRED)

# the library
add_library(
Expand Down Expand Up @@ -121,7 +122,8 @@ target_link_libraries(
google-cloud-cpp::rpc_status_protos
google-cloud-cpp::common
gRPC::grpc++
gRPC::grpc)
gRPC::grpc
nlohmann_json::nlohmann_json)
google_cloud_cpp_add_common_options(google_cloud_cpp_grpc_utils)
target_include_directories(
google_cloud_cpp_grpc_utils PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>
Expand Down Expand Up @@ -276,6 +278,7 @@ if (BUILD_TESTING)
# List the unit tests, then setup the targets and dependencies.
set(google_cloud_cpp_grpc_utils_integration_tests
# cmake-format: sort
internal/grpc_gdch_service_account_integration_test.cc
internal/grpc_impersonate_service_account_integration_test.cc)

# Export the list of unit and integration tests so the Bazel BUILD file can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"""Automatically generated unit tests list - DO NOT EDIT."""

google_cloud_cpp_grpc_utils_integration_tests = [
"internal/grpc_gdch_service_account_integration_test.cc",
"internal/grpc_impersonate_service_account_integration_test.cc",
]
112 changes: 112 additions & 0 deletions google/cloud/internal/grpc_gdch_service_account_integration_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright 2026 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/completion_queue.h"
#include "google/cloud/credentials.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/internal/unified_grpc_credentials.h"
#include "google/cloud/testing_util/scoped_environment.h"
#include "google/cloud/testing_util/status_matchers.h"
#include <gmock/gmock.h>
#if __has_include(<grpcpp/version_info.h>)
#include <grpcpp/version_info.h>
#endif
#include <fstream>

namespace google {
namespace cloud {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace internal {
namespace {

using ::google::cloud::testing_util::IsOk;
using ::google::cloud::testing_util::ScopedEnvironment;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::NotNull;

TEST(GrpcGdchServiceAccountIntegrationTest,
RetrievesBearerTokenFromMemoryInAdhocEnvironment) {
#if !defined(GRPC_CPP_VERSION_MAJOR) || \
(GRPC_CPP_VERSION_MAJOR < 1 || \
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR < 84))
GTEST_SKIP() << "GDCH credentials require gRPC >= 1.84.0";
#endif
std::optional<std::string> key_file_env = GetEnv("GRPC_TEST_GDCH_KEY_FILE");
std::optional<std::string> audience_env = GetEnv("GRPC_TEST_GDCH_AUDIENCE");
if (!key_file_env.has_value() || !audience_env.has_value()) GTEST_SKIP();

std::ifstream is(*key_file_env);
std::string contents = std::string{std::istreambuf_iterator<char>{is}, {}};
ASSERT_THAT(contents, Not(IsEmpty()));

CompletionQueue cq;

std::shared_ptr<Credentials> creds =
MakeGDCHServiceAccountCredentials(contents, *audience_env);
ASSERT_THAT(creds, NotNull());

std::shared_ptr<GrpcAuthenticationStrategy> auth =
CreateAuthenticationStrategy(*creds, cq);
ASSERT_THAT(auth, NotNull());

grpc::ClientContext context;
Status status = auth->ConfigureContext(context);
EXPECT_THAT(status, IsOk());

std::shared_ptr<grpc::Channel> channel =
auth->CreateChannel("localhost:443", grpc::ChannelArguments{});
EXPECT_THAT(channel, NotNull());
}

TEST(GrpcGdchServiceAccountIntegrationTest,
RetrievesBearerTokenFromFileInAdhocEnvironment) {
#if !defined(GRPC_CPP_VERSION_MAJOR) || \
(GRPC_CPP_VERSION_MAJOR < 1 || \
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR < 84))
GTEST_SKIP() << "GDCH credentials require gRPC >= 1.84.0";
#endif
std::optional<std::string> key_file_env = GetEnv("GRPC_TEST_GDCH_KEY_FILE");
std::optional<std::string> audience_env = GetEnv("GRPC_TEST_GDCH_AUDIENCE");
if (!key_file_env.has_value() || !audience_env.has_value()) GTEST_SKIP();

std::ifstream is(*key_file_env);
std::string contents = std::string{std::istreambuf_iterator<char>{is}, {}};
ASSERT_THAT(contents, Not(IsEmpty()));

CompletionQueue cq;

ScopedEnvironment env("GOOGLE_APPLICATION_CREDENTIALS", key_file_env);
std::shared_ptr<Credentials> creds =
MakeGDCHServiceAccountCredentials(*audience_env);
ASSERT_THAT(creds, NotNull());

std::shared_ptr<GrpcAuthenticationStrategy> auth =
CreateAuthenticationStrategy(*creds, cq);
ASSERT_THAT(auth, NotNull());

grpc::ClientContext context;
Status status = auth->ConfigureContext(context);
EXPECT_THAT(status, IsOk());

std::shared_ptr<grpc::Channel> channel =
auth->CreateChannel("localhost:443", grpc::ChannelArguments{});
EXPECT_THAT(channel, NotNull());
}

} // namespace
} // namespace internal
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace cloud
} // namespace google
78 changes: 73 additions & 5 deletions google/cloud/internal/unified_grpc_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
#include "google/cloud/internal/grpc_impersonate_service_account.h"
#include "google/cloud/internal/grpc_service_account_authentication.h"
#include <grpcpp/security/credentials.h>
#if __has_include(<grpcpp/version_info.h>)
#include <grpcpp/version_info.h>
#endif
#include <nlohmann/json.hpp>
#include <fstream>

namespace {
Expand Down Expand Up @@ -161,12 +165,76 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
"or Access Token Credentials instead.",
GCP_ERROR_INFO())});
}
void visit(GDCHServiceAccountConfig const&) override {
void visit(GDCHServiceAccountConfig const& cfg) override {
#if defined(GRPC_CPP_VERSION_MAJOR) && \
(GRPC_CPP_VERSION_MAJOR > 1 || \
(GRPC_CPP_VERSION_MAJOR == 1 && GRPC_CPP_VERSION_MINOR >= 84))
std::string json_contents;
if (cfg.file_path().has_value()) {
std::ifstream is(*cfg.file_path());
if (!is.is_open()) {
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{UnknownError(
"Cannot open credentials file " + *cfg.file_path(),
GCP_ERROR_INFO())});
return;
}
json_contents = std::string{std::istreambuf_iterator<char>{is}, {}};
} else if (!cfg.json_object().empty()) {
json_contents = cfg.json_object();
} else {
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{
InternalError("GDCHServiceAccountConfig has neither "
"json_object nor file_path",
GCP_ERROR_INFO())});
return;
}

std::shared_ptr<grpc::CallCredentials> gdch_creds =
grpc::GDCHServiceAccountCredentials(json_contents, cfg.audience());
if (!gdch_creds) {
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{InternalError(
"Error creating grpc::GDCHServiceAccountCredentials",
GCP_ERROR_INFO())});
return;
}

std::string ca_cert_path;
nlohmann::json j = nlohmann::json::parse(json_contents, nullptr, false);
if (!j.is_discarded() && j.is_object()) {
auto it = j.find("ca_cert_path");
if (it != j.end() && it->is_string()) {
ca_cert_path = it->get<std::string>();
}
}
Comment thread
scotthart marked this conversation as resolved.
grpc::SslCredentialsOptions ssl_options;
if (!ca_cert_path.empty()) {
std::ifstream is(ca_cert_path);
if (!is.is_open()) {
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{UnknownError(
"Cannot open CA certificate file " + ca_cert_path,
GCP_ERROR_INFO())});
return;
}
ssl_options.pem_root_certs =
std::string{std::istreambuf_iterator<char>{is.rdbuf()}, {}};
} else {
Comment thread
scotthart marked this conversation as resolved.
std::optional<std::string> cainfo = LoadCAInfo(options);
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
}
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::CompositeChannelCredentials(grpc::SslCredentials(ssl_options),
gdch_creds));
#else
(void)cfg;
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{
UnimplementedError("GDCHServiceAccountCredentials are not yet "
"supported for gRPC endpoints",
GCP_ERROR_INFO())});
ErrorCredentialsConfig{UnimplementedError(
"GDCHServiceAccountCredentials require gRPC v1.84.0 or greater",
GCP_ERROR_INFO())});
#endif
}

} visitor(std::move(cq), std::move(options));
Expand Down
Loading
Loading