Skip to content

Commit a0ad519

Browse files
authored
impl(oauth2): add support for creating GDCH credentials for use with REST endpoints (googleapis#16129)
1 parent 85bfaff commit a0ad519

12 files changed

Lines changed: 245 additions & 12 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ the APIs in these libraries are stable, and are ready for production use.
2020

2121
- [Network Security API] has been updated with several additional services ([#16122](https://github.com/googleapis/google-cloud-cpp/pull/16122))
2222

23+
### Auth
24+
25+
- [Google Distributed Cloud Hosting (GDCH) Service Account credentials](https://docs.cloud.google.com/distributed-cloud/hosted/docs/latest/gdcag/platform/pa-user/service-identity)
26+
are now supported for REST endpoints. Support for gRPC endpoints will be
27+
available in a future release.
28+
29+
2330
## v3.5.0 - 2026-05
2431

2532
### [Bigtable](/google/cloud/bigtable/README.md)

google/cloud/credentials.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ std::shared_ptr<Credentials> MakeComputeEngineCredentials(Options opts) {
7474
std::move(opts));
7575
}
7676

77+
std::shared_ptr<Credentials> MakeGDCHServiceAccountCredentials(
78+
std::string json_object, std::string audience, Options opts) {
79+
return std::make_shared<internal::GDCHServiceAccountConfig>(
80+
std::move(json_object), std::move(audience), std::move(opts));
81+
}
82+
83+
std::shared_ptr<Credentials> MakeGDCHServiceAccountCredentials(
84+
std::string audience, Options opts) {
85+
return std::make_shared<internal::GDCHServiceAccountConfig>(
86+
std::move(audience), std::move(opts));
87+
}
88+
7789
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
7890
} // namespace cloud
7991
} // namespace google

google/cloud/credentials.h

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,39 @@ std::shared_ptr<Credentials> MakeApiKeyCredentials(std::string api_key,
454454
*/
455455
std::shared_ptr<Credentials> MakeComputeEngineCredentials(Options opts = {});
456456

457+
/**
458+
* Creates credentials for a Google Distributed Cloud Hosting (GDCH) Service
459+
* Account.
460+
*
461+
* @see https://docs.cloud.google.com/distributed-cloud/hosted/docs/latest/gdcag
462+
* for more information on GDCH air-gapped environments.
463+
*
464+
*
465+
* @see https://cloud.google.com/docs/authentication for more information on
466+
* authentication in GCP.
467+
*
468+
* @see https://cloud.google.com/docs/authentication/client-libraries for more
469+
* information on authentication for client libraries.
470+
*
471+
* [aip/4115]: https://google.aip.dev/auth/4115
472+
*
473+
* @ingroup guac
474+
*
475+
* @param json_object service account configuration as a JSON string. If
476+
* omitted, the contents of the file at GOOGLE_APPLICATION_CREDENTIALS is
477+
* used.
478+
*
479+
* @param audience authentication endpoint for the service identity.
480+
*
481+
* @param opts optional configuration values. Note that the effect of these
482+
* parameters depends on the underlying transport. For example,
483+
* `LoggingComponentsOption` is ignored by gRPC-based services.
484+
*/
485+
std::shared_ptr<Credentials> MakeGDCHServiceAccountCredentials(
486+
std::string json_object, std::string audience, Options opts = {});
487+
std::shared_ptr<Credentials> MakeGDCHServiceAccountCredentials(
488+
std::string audience, Options opts = {});
489+
457490
/**
458491
* Configure the delegates for `MakeImpersonateServiceAccountCredentials()`
459492
*

google/cloud/internal/credentials_impl.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "google/cloud/internal/credentials_impl.h"
1616
#include "google/cloud/common_options.h"
17+
#include "google/cloud/internal/getenv.h"
1718
#include "google/cloud/internal/populate_common_options.h"
1819
#include <chrono>
1920

@@ -117,6 +118,19 @@ AuthorizedUserConfig::AuthorizedUserConfig(std::string json_object,
117118
: json_object_(std::move(json_object)),
118119
options_(PopulateAuthOptions(std::move(opts))) {}
119120

121+
GDCHServiceAccountConfig::GDCHServiceAccountConfig(std::string json_object,
122+
std::string audience,
123+
Options opts)
124+
: json_object_(std::move(json_object)),
125+
audience_(std::move(audience)),
126+
options_(PopulateAuthOptions(std::move(opts))) {}
127+
128+
GDCHServiceAccountConfig::GDCHServiceAccountConfig(std::string audience,
129+
Options opts)
130+
: file_path_(GetEnv("GOOGLE_APPLICATION_CREDENTIALS")),
131+
audience_(std::move(audience)),
132+
options_(PopulateAuthOptions(std::move(opts))) {}
133+
120134
} // namespace internal
121135
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
122136
} // namespace cloud

google/cloud/internal/credentials_impl.h

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
#include "google/cloud/options.h"
2121
#include "google/cloud/status_or.h"
2222
#include "google/cloud/version.h"
23-
#include "absl/types/optional.h"
2423
#include <chrono>
2524
#include <functional>
2625
#include <memory>
26+
#include <optional>
2727
#include <string>
2828
#include <vector>
2929

@@ -42,6 +42,7 @@ class ExternalAccountConfig;
4242
class ApiKeyConfig;
4343
class ComputeEngineCredentialsConfig;
4444
class AuthorizedUserConfig;
45+
class GDCHServiceAccountConfig;
4546

4647
std::shared_ptr<Credentials> MakeErrorCredentials(Status error_status);
4748

@@ -70,6 +71,7 @@ class CredentialsVisitor {
7071
virtual void visit(ApiKeyConfig const&) = 0;
7172
virtual void visit(ComputeEngineCredentialsConfig const&) = 0;
7273
virtual void visit(AuthorizedUserConfig const&) = 0;
74+
virtual void visit(GDCHServiceAccountConfig const&) = 0;
7375

7476
static void dispatch(Credentials const& credentials,
7577
CredentialsVisitor& visitor);
@@ -161,20 +163,18 @@ class ServiceAccountConfig : public Credentials {
161163
// Only one of json_object or file_path should have a value.
162164
// TODO(#15886): Use the C++ type system to make better constructors that
163165
// enforces this comment.
164-
ServiceAccountConfig(absl::optional<std::string> json_object,
165-
absl::optional<std::string> file_path, Options opts);
166+
ServiceAccountConfig(std::optional<std::string> json_object,
167+
std::optional<std::string> file_path, Options opts);
166168

167-
absl::optional<std::string> const& json_object() const {
168-
return json_object_;
169-
}
170-
absl::optional<std::string> const& file_path() const { return file_path_; }
169+
std::optional<std::string> const& json_object() const { return json_object_; }
170+
std::optional<std::string> const& file_path() const { return file_path_; }
171171
Options const& options() const { return options_; }
172172

173173
private:
174174
void dispatch(CredentialsVisitor& v) const override { v.visit(*this); }
175175

176-
absl::optional<std::string> json_object_;
177-
absl::optional<std::string> file_path_;
176+
std::optional<std::string> json_object_;
177+
std::optional<std::string> file_path_;
178178
Options options_;
179179
};
180180

@@ -235,6 +235,28 @@ class AuthorizedUserConfig : public Credentials {
235235
Options options_;
236236
};
237237

238+
class GDCHServiceAccountConfig : public Credentials {
239+
public:
240+
GDCHServiceAccountConfig(std::string json_object, std::string audience,
241+
Options opts);
242+
GDCHServiceAccountConfig(std::string audience, Options opts);
243+
244+
~GDCHServiceAccountConfig() override = default;
245+
246+
std::optional<std::string> const& file_path() const { return file_path_; }
247+
std::string const& json_object() const { return json_object_; }
248+
std::string const& audience() const { return audience_; }
249+
Options const& options() const { return options_; }
250+
251+
private:
252+
void dispatch(CredentialsVisitor& v) const override { v.visit(*this); }
253+
254+
std::optional<std::string> file_path_ = std::nullopt;
255+
std::string json_object_;
256+
std::string audience_;
257+
Options options_;
258+
};
259+
238260
/// A helper function to initialize Auth options.
239261
Options PopulateAuthOptions(Options options);
240262

google/cloud/internal/credentials_impl_test.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,17 @@ TEST(Credentials, AuthorizedUserCredentials) {
135135
ElementsAre("scope1", "scope2"));
136136
}
137137

138+
TEST(Credentials, GDCHServiceAccountCredentials) {
139+
TestCredentialsVisitor visitor;
140+
141+
auto credentials =
142+
MakeGDCHServiceAccountCredentials("test-json", "test-audience");
143+
CredentialsVisitor::dispatch(*credentials, visitor);
144+
EXPECT_EQ("GDCHServiceAccountConfig", visitor.name);
145+
EXPECT_EQ("test-json", visitor.json_object);
146+
EXPECT_EQ("test-audience", visitor.audience);
147+
}
148+
138149
TEST(PopulateAuthOptions, EmptyOptions) {
139150
auto result_options = PopulateAuthOptions(Options{});
140151

google/cloud/internal/unified_grpc_credentials.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
161161
"or Access Token Credentials instead.",
162162
GCP_ERROR_INFO())});
163163
}
164+
void visit(GDCHServiceAccountConfig const&) override {
165+
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
166+
ErrorCredentialsConfig{
167+
UnimplementedError("GDCHServiceAccountCredentials are not yet "
168+
"supported for gRPC endpoints",
169+
GCP_ERROR_INFO())});
170+
}
164171

165172
} visitor(std::move(cq), std::move(options));
166173

google/cloud/internal/unified_rest_credentials.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "google/cloud/internal/oauth2_decorate_credentials.h"
2525
#include "google/cloud/internal/oauth2_error_credentials.h"
2626
#include "google/cloud/internal/oauth2_external_account_credentials.h"
27+
#include "google/cloud/internal/oauth2_gdch_service_account_credentials.h"
2728
#include "google/cloud/internal/oauth2_google_credentials.h"
2829
#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h"
2930
#include "google/cloud/internal/oauth2_service_account_credentials.h"
@@ -41,6 +42,7 @@ using ::google::cloud::internal::ComputeEngineCredentialsConfig;
4142
using ::google::cloud::internal::CredentialsVisitor;
4243
using ::google::cloud::internal::ErrorCredentialsConfig;
4344
using ::google::cloud::internal::ExternalAccountConfig;
45+
using ::google::cloud::internal::GDCHServiceAccountConfig;
4446
using ::google::cloud::internal::GoogleDefaultCredentialsConfig;
4547
using ::google::cloud::internal::ImpersonateServiceAccountConfig;
4648
using ::google::cloud::internal::InsecureCredentialsConfig;
@@ -154,6 +156,30 @@ std::shared_ptr<oauth2_internal::Credentials> MapCredentials(
154156
result =
155157
Decorate(std::move(creds), std::move(client_factory_), cfg.options());
156158
}
159+
void visit(GDCHServiceAccountConfig const& cfg) override {
160+
auto options = cfg.options();
161+
StatusOr<std::unique_ptr<oauth2_internal::Credentials>> creds;
162+
if (cfg.file_path().has_value()) {
163+
creds =
164+
oauth2_internal::GDCHServiceAccountCredentials::CreateFromFilePath(
165+
*cfg.file_path(), cfg.audience(), options, client_factory_);
166+
} else if (!cfg.json_object().empty()) {
167+
creds = oauth2_internal::GDCHServiceAccountCredentials::
168+
CreateFromJsonContents(cfg.json_object(), cfg.audience(), options,
169+
client_factory_);
170+
} else {
171+
creds = internal::InvalidArgumentError(
172+
"GDCH ServiceAccount Credentials require either a JSON object or "
173+
"the "
174+
"GOOGLE_APPLICATION_CREDENTIALS environment variable to be set");
175+
}
176+
if (!creds.ok()) {
177+
result = MakeErrorCredentials(std::move(creds).status());
178+
} else {
179+
result = Decorate(*std::move(creds), std::move(client_factory_),
180+
std::move(options));
181+
}
182+
}
157183

158184
void visit(AuthorizedUserConfig const& cfg) override {
159185
auto info = oauth2_internal::ParseAuthorizedUserCredentials(

google/cloud/internal/unified_rest_credentials_integration_test.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
3434
namespace {
3535

3636
using ::google::cloud::testing_util::IsOk;
37+
using ::google::cloud::testing_util::IsOkAndHolds;
3738
using ::google::cloud::testing_util::ScopedEnvironment;
39+
using ::testing::IsEmpty;
40+
using ::testing::Not;
41+
using ::testing::NotNull;
3842

3943
auto constexpr kEndpointThatUsesRAB = "storage.googleapis.com";
4044

@@ -355,6 +359,26 @@ TEST(UnifiedRestCredentialsIntegrationTest, StorageSelfSignedJWT) {
355359
MakeServiceAccountCredentials(contents))));
356360
}
357361

362+
MATCHER(AccessTokenIsSTSBearer, "access token is STS Bearer") {
363+
return absl::StartsWith(arg.token, "STS-Bearer-");
364+
}
365+
366+
TEST(UnifiedRestCredentialsIntegrationTest, GDCHServiceAccountCredentials) {
367+
auto key_file_env =
368+
internal::GetEnv("GOOGLE_CLOUD_CPP_REST_TEST_GDCH_KEY_FILE");
369+
if (!key_file_env.has_value()) GTEST_SKIP();
370+
std::ifstream is(*key_file_env);
371+
auto contents = std::string{std::istreambuf_iterator<char>{is}, {}};
372+
ASSERT_THAT(contents, Not(IsEmpty()));
373+
374+
std::string const audience = "global-api";
375+
auto gdch_creds = MakeGDCHServiceAccountCredentials(contents, audience);
376+
ASSERT_THAT(gdch_creds, NotNull());
377+
auto oauth2_creds = MapCredentials(*gdch_creds);
378+
EXPECT_THAT(oauth2_creds->GetToken(std::chrono::system_clock::now()),
379+
IsOkAndHolds(AccessTokenIsSTSBearer()));
380+
}
381+
358382
} // namespace
359383
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
360384
} // namespace rest_internal

0 commit comments

Comments
 (0)