Skip to content

Commit d4beefe

Browse files
authored
impl(rest): add unified credential support (googleapis#8409)
* impl(rest): add unified credential support
1 parent 033fcd3 commit d4beefe

9 files changed

+253
-8
lines changed

ci/cloudbuild/builds/lib/integration.sh

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ function integration::bazel_args() {
7474
"--test_env=GOOGLE_CLOUD_CPP_BIGTABLE_TEST_SERVICE_ACCOUNT=${GOOGLE_CLOUD_CPP_BIGTABLE_TEST_SERVICE_ACCOUNT}"
7575
"--test_env=ENABLE_BIGTABLE_ADMIN_INTEGRATION_TESTS=${ENABLE_BIGTABLE_ADMIN_INTEGRATION_TESTS:-no}"
7676

77+
# Rest
78+
"--test_env=GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT=${GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT}"
79+
7780
# Storage
7881
"--test_env=GOOGLE_CLOUD_CPP_STORAGE_GRPC_CONFIG=${GOOGLE_CLOUD_CPP_STORAGE_GRPC_CONFIG:-}"
7982
"--test_env=GOOGLE_CLOUD_CPP_STORAGE_TEST_BUCKET_NAME=${GOOGLE_CLOUD_CPP_STORAGE_TEST_BUCKET_NAME}"
@@ -109,6 +112,7 @@ function integration::bazel_args() {
109112
gsutil cp "${SECRETS_BUCKET}/${key_base}.json" "${KEY_DIR}/${key_base}.json"
110113
gsutil cp "${SECRETS_BUCKET}/${key_base}.p12" "${KEY_DIR}/${key_base}.p12"
111114
args+=(
115+
"--test_env=GOOGLE_CLOUD_CPP_REST_TEST_KEY_FILE_JSON=${KEY_DIR}/${key_base}.json"
112116
"--test_env=GOOGLE_CLOUD_CPP_BIGTABLE_TEST_KEY_FILE_JSON=${KEY_DIR}/${key_base}.json"
113117
"--test_env=GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_JSON=${KEY_DIR}/${key_base}.json"
114118
"--test_env=GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_P12=${KEY_DIR}/${key_base}.p12"
@@ -149,6 +153,8 @@ function integration::bazel_with_emulators() {
149153
"google/cloud/iam/..."
150154
# Logging integration tests
151155
"google/cloud/logging/..."
156+
# Unified Rest Credentials test
157+
"google/cloud:internal_unified_rest_credentials_integration_test"
152158
)
153159

154160
tag_filters="integration-test"

ci/etc/integration-tests-config.ps1

+3
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ $env:GOOGLE_CLOUD_CPP_IAM_CREDENTIALS_TEST_SERVICE_ACCOUNT="iam-credentials-test
7070
$env:GOOGLE_CLOUD_CPP_IAM_TEST_SERVICE_ACCOUNT="iam-test-sa@${env:GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com"
7171
$env:GOOGLE_CLOUD_CPP_IAM_INVALID_TEST_SERVICE_ACCOUNT="invalid-test-account@cloud-cpp-testing-resources.iam.gserviceaccount.com"
7272

73+
# Rest configuration parameters
74+
$env:GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT="kokoro-run@${env:GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com"
75+
7376
# To run google/cloud/gameservices' quickstart
7477
$env:GOOGLE_CLOUD_CPP_GAMESERVICES_TEST_LOCATION="global"
7578
$env:GOOGLE_CLOUD_CPP_GAMESERVICES_TEST_REALM="test-realm"

ci/etc/integration-tests-config.sh

+3
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export GOOGLE_CLOUD_CPP_TEST_HELLO_WORLD_SERVICE_ACCOUNT="hello-world-caller@${G
9292
# The URL for the HTTP Hello World service, typically set by the CI scripts
9393
export GOOGLE_CLOUD_CPP_TEST_HELLO_WORLD_HTTP_URL=""
9494

95+
# Rest configuration parameters
96+
export GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT="kokoro-run@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com"
97+
9598
# To run google/cloud/gameservices' quickstart
9699
export GOOGLE_CLOUD_CPP_GAMESERVICES_TEST_LOCATION="global"
97100
export GOOGLE_CLOUD_CPP_GAMESERVICES_TEST_REALM="test-realm"

google/cloud/BUILD.bazel

+17-2
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ load(":google_cloud_cpp_rest_internal_unit_tests.bzl", "google_cloud_cpp_rest_in
195195
],
196196
) for test in google_cloud_cpp_rest_internal_unit_tests]
197197

198-
load(":google_cloud_cpp_rest_internal_integration_tests.bzl", "google_cloud_cpp_rest_internal_integration_tests")
198+
load(":google_cloud_cpp_rest_internal_emulator_integration_tests.bzl", "google_cloud_cpp_rest_internal_emulator_integration_tests")
199199

200200
[cc_test(
201201
name = test.replace("/", "_").replace(".cc", ""),
@@ -208,4 +208,19 @@ load(":google_cloud_cpp_rest_internal_integration_tests.bzl", "google_cloud_cpp_
208208
"//google/cloud/testing_util:google_cloud_cpp_testing",
209209
"@com_google_googletest//:gtest_main",
210210
],
211-
) for test in google_cloud_cpp_rest_internal_integration_tests]
211+
) for test in google_cloud_cpp_rest_internal_emulator_integration_tests]
212+
213+
load(":google_cloud_cpp_rest_internal_production_integration_tests.bzl", "google_cloud_cpp_rest_internal_production_integration_tests")
214+
215+
[cc_test(
216+
name = test.replace("/", "_").replace(".cc", ""),
217+
srcs = [test],
218+
tags = [
219+
"integration-test",
220+
],
221+
deps = [
222+
":google_cloud_cpp_rest_internal",
223+
"//google/cloud/testing_util:google_cloud_cpp_testing",
224+
"@com_google_googletest//:gtest_main",
225+
],
226+
) for test in google_cloud_cpp_rest_internal_production_integration_tests]

google/cloud/CMakeLists.txt

+24-5
Original file line numberDiff line numberDiff line change
@@ -750,28 +750,47 @@ if (GOOGLE_CLOUD_CPP_ENABLE_REST)
750750
internal/rest_response_test.cc
751751
internal/unified_rest_credentials_test.cc)
752752

753-
# List the integration tests, then setup the targets and dependencies.
754-
set(google_cloud_cpp_rest_internal_integration_tests
753+
# List the emulator integration tests, then setup the targets and
754+
# dependencies.
755+
set(google_cloud_cpp_rest_internal_emulator_integration_tests
755756
# cmake-format: sort
756757
internal/rest_client_integration_test.cc)
757758

759+
# List the production integration tests, then setup the targets and
760+
# dependencies.
761+
set(google_cloud_cpp_rest_internal_production_integration_tests
762+
# cmake-format: sort
763+
internal/unified_rest_credentials_integration_test.cc)
764+
758765
# Export the list of unit and integration tests so the Bazel BUILD file
759766
# can pick them up.
760767
export_list_to_bazel(
761768
"google_cloud_cpp_rest_internal_unit_tests.bzl"
762769
"google_cloud_cpp_rest_internal_unit_tests" YEAR 2021)
763770
export_list_to_bazel(
764-
"google_cloud_cpp_rest_internal_integration_tests.bzl"
765-
"google_cloud_cpp_rest_internal_integration_tests" YEAR 2022)
771+
"google_cloud_cpp_rest_internal_emulator_integration_tests.bzl"
772+
"google_cloud_cpp_rest_internal_emulator_integration_tests" YEAR
773+
2022)
774+
export_list_to_bazel(
775+
"google_cloud_cpp_rest_internal_production_integration_tests.bzl"
776+
"google_cloud_cpp_rest_internal_production_integration_tests" YEAR
777+
2022)
766778

767779
foreach (fname ${google_cloud_cpp_rest_internal_unit_tests})
768780
google_cloud_cpp_rest_internal_add_test("${fname}" "")
769781
endforeach ()
770782

771-
foreach (fname ${google_cloud_cpp_rest_internal_integration_tests})
783+
foreach (fname
784+
${google_cloud_cpp_rest_internal_emulator_integration_tests})
772785
google_cloud_cpp_rest_internal_add_test(
773786
"${fname}" "integration-test;integration-test-emulator")
774787
endforeach ()
788+
789+
foreach (fname
790+
${google_cloud_cpp_rest_internal_production_integration_tests})
791+
google_cloud_cpp_rest_internal_add_test(
792+
"${fname}" "integration-test;integration-test-production")
793+
endforeach ()
775794
endif ()
776795

777796
# Install the libraries and headers in the locations determined by
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
# DO NOT EDIT -- GENERATED BY CMake -- Change the CMakeLists.txt file if needed
16+
17+
"""Automatically generated unit tests list - DO NOT EDIT."""
18+
19+
google_cloud_cpp_rest_internal_emulator_integration_tests = [
20+
"internal/rest_client_integration_test.cc",
21+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
# DO NOT EDIT -- GENERATED BY CMake -- Change the CMakeLists.txt file if needed
16+
17+
"""Automatically generated unit tests list - DO NOT EDIT."""
18+
19+
google_cloud_cpp_rest_internal_production_integration_tests = [
20+
"internal/unified_rest_credentials_integration_test.cc",
21+
]

google/cloud/internal/rest_client.cc

+8-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
#include "google/cloud/internal/curl_handle_factory.h"
2121
#include "google/cloud/internal/curl_impl.h"
2222
#include "google/cloud/internal/curl_options.h"
23+
#include "google/cloud/internal/oauth2_google_credentials.h"
2324
#include "google/cloud/internal/rest_options.h"
25+
#include "google/cloud/internal/unified_rest_credentials.h"
2426
#include "google/cloud/log.h"
2527
#include "absl/strings/match.h"
2628
#include "absl/strings/strip.h"
@@ -99,7 +101,12 @@ StatusOr<std::unique_ptr<CurlImpl>> CurlRestClient::CreateCurlImpl(
99101
auto handle = GetCurlHandle(handle_factory_);
100102
auto impl =
101103
absl::make_unique<CurlImpl>(std::move(handle), handle_factory_, options_);
102-
// TODO(#8130): Add Authorization header from UnifiedCredentialsOption
104+
if (options_.has<UnifiedCredentialsOption>()) {
105+
auto credentials = MapCredentials(options_.get<UnifiedCredentialsOption>());
106+
auto auth_header = credentials->AuthorizationHeader();
107+
if (!auth_header.ok()) return std::move(auth_header).status();
108+
impl->SetHeader(auth_header.value());
109+
}
103110
impl->SetHeader(HostHeader(options_, endpoint_address_));
104111
impl->SetHeader(x_goog_api_client_header_);
105112
impl->SetHeaders(request);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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/credentials.h"
16+
#include "google/cloud/internal/curl_options.h"
17+
#include "google/cloud/internal/getenv.h"
18+
#include "google/cloud/internal/oauth2_google_credentials.h"
19+
#include "google/cloud/internal/oauth2_minimal_iam_credentials_rest.h"
20+
#include "google/cloud/internal/rest_client.h"
21+
#include "google/cloud/internal/setenv.h"
22+
#include "google/cloud/log.h"
23+
#include "google/cloud/testing_util/scoped_environment.h"
24+
#include "google/cloud/testing_util/status_matchers.h"
25+
#include "absl/strings/str_split.h"
26+
#include <gmock/gmock.h>
27+
#include <nlohmann/json.hpp>
28+
#include <fstream>
29+
30+
namespace google {
31+
namespace cloud {
32+
namespace rest_internal {
33+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
34+
namespace {
35+
36+
using ::google::cloud::testing_util::IsOk;
37+
using ::google::cloud::testing_util::ScopedEnvironment;
38+
using ::google::cloud::testing_util::StatusIs;
39+
40+
StatusOr<std::unique_ptr<RestResponse>> RetryRestRequest(
41+
std::function<StatusOr<std::unique_ptr<RestResponse>>()> const& request,
42+
StatusCode expected_status = StatusCode::kOk) {
43+
auto delay = std::chrono::seconds(1);
44+
StatusOr<std::unique_ptr<RestResponse>> response;
45+
for (auto i = 0; i != 3; ++i) {
46+
response = request();
47+
if (response.status().code() == expected_status) return response;
48+
std::this_thread::sleep_for(delay);
49+
delay *= 2;
50+
}
51+
return response;
52+
}
53+
54+
void MakeRestRpcCall(StatusCode expected_status, Options options = {}) {
55+
std::string bigquery_endpoint = "https://bigquery.googleapis.com";
56+
auto client = GetPooledRestClient(bigquery_endpoint, std::move(options));
57+
RestRequest request;
58+
request.SetPath("bigquery/v2/projects/bigquery-public-data/datasets");
59+
request.AddQueryParameter({"maxResults", "10"});
60+
auto response = RetryRestRequest([&] { return client->Get(request); });
61+
ASSERT_THAT(response, IsOk());
62+
auto response_payload = std::move(**response).ExtractPayload();
63+
auto response_json = ReadAll(std::move(response_payload));
64+
ASSERT_THAT(response_json, StatusIs(expected_status));
65+
if (response_json.ok()) {
66+
auto parsed_response =
67+
nlohmann::json::parse(*response_json, nullptr, false);
68+
ASSERT_TRUE(parsed_response.is_object());
69+
auto kind = parsed_response.find("kind");
70+
ASSERT_NE(kind, parsed_response.end());
71+
EXPECT_EQ(std::string(kind.value()), "bigquery#datasetList");
72+
}
73+
}
74+
75+
TEST(UnifiedRestCredentialsIntegrationTest, InsecureCredentials) {
76+
std::string bigquery_endpoint = "https://bigquery.googleapis.com";
77+
auto client = GetPooledRestClient(
78+
bigquery_endpoint,
79+
Options{}.set<UnifiedCredentialsOption>(MakeInsecureCredentials()));
80+
RestRequest request;
81+
request.SetPath("bigquery/v2/projects/bigquery-public-data/datasets");
82+
request.AddQueryParameter({"maxResults", "10"});
83+
auto response = RetryRestRequest([&] { return client->Get(request); });
84+
EXPECT_THAT(response, StatusIs(StatusCode::kOk));
85+
auto response_payload = std::move(**response).ExtractPayload();
86+
auto response_json = ReadAll(std::move(response_payload));
87+
ASSERT_THAT(response_json, StatusIs(StatusCode::kUnauthenticated));
88+
}
89+
90+
TEST(UnifiedRestCredentialsIntegrationTest, GoogleDefaultCredentials) {
91+
MakeRestRpcCall(StatusCode::kOk, Options{}.set<UnifiedCredentialsOption>(
92+
MakeGoogleDefaultCredentials()));
93+
}
94+
95+
TEST(UnifiedRestCredentialsIntegrationTest, AccessTokenCredentials) {
96+
auto env = internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_KEY_FILE_JSON");
97+
ASSERT_TRUE(env.has_value());
98+
std::string key_file = std::move(*env);
99+
ScopedEnvironment google_app_creds_override_env_var(
100+
"GOOGLE_APPLICATION_CREDENTIALS", key_file);
101+
auto default_creds = oauth2_internal::GoogleDefaultCredentials();
102+
ASSERT_THAT(default_creds, IsOk());
103+
auto iam_creds =
104+
oauth2_internal::MakeMinimalIamCredentialsRestStub(*default_creds);
105+
oauth2_internal::GenerateAccessTokenRequest request;
106+
request.lifetime = std::chrono::hours(1);
107+
auto service_account =
108+
internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT");
109+
ASSERT_TRUE(service_account.has_value());
110+
request.service_account = std::move(*service_account);
111+
request.scopes.emplace_back("https://www.googleapis.com/auth/cloud-platform");
112+
auto token = iam_creds->GenerateAccessToken(request);
113+
auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1);
114+
MakeRestRpcCall(StatusCode::kOk,
115+
Options{}.set<UnifiedCredentialsOption>(
116+
MakeAccessTokenCredentials(token->token, expiration)));
117+
}
118+
119+
TEST(UnifiedRestCredentialsIntegrationTest,
120+
ImpersonateServiceAccountCredentials) {
121+
auto env = internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_KEY_FILE_JSON");
122+
ASSERT_TRUE(env.has_value());
123+
std::string key_file = std::move(*env);
124+
ScopedEnvironment google_app_creds_override_env_var(
125+
"GOOGLE_APPLICATION_CREDENTIALS", key_file);
126+
auto service_account =
127+
internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_SIGNING_SERVICE_ACCOUNT");
128+
ASSERT_TRUE(service_account.has_value());
129+
MakeRestRpcCall(StatusCode::kOk,
130+
Options{}.set<UnifiedCredentialsOption>(
131+
MakeImpersonateServiceAccountCredentials(
132+
MakeGoogleDefaultCredentials(), *service_account)));
133+
}
134+
135+
TEST(UnifiedRestCredentialsIntegrationTest, ServiceAccountCredentials) {
136+
auto env = internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_KEY_FILE_JSON");
137+
ASSERT_TRUE(env.has_value());
138+
std::string key_file = std::move(*env);
139+
std::ifstream is(key_file);
140+
auto contents = std::string{std::istreambuf_iterator<char>{is}, {}};
141+
MakeRestRpcCall(StatusCode::kOk,
142+
Options{}.set<UnifiedCredentialsOption>(
143+
MakeServiceAccountCredentials(contents)));
144+
}
145+
146+
} // namespace
147+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
148+
} // namespace rest_internal
149+
} // namespace cloud
150+
} // namespace google

0 commit comments

Comments
 (0)