From 6f551a7bf6ad69fcbaf0bfae358b4daeba4eecb2 Mon Sep 17 00:00:00 2001 From: Sachin Purohit Date: Wed, 6 May 2026 14:04:58 +0000 Subject: [PATCH] feat: implemented MakeUserAccountCredentials --- google/cloud/credentials.cc | 10 +++++ google/cloud/credentials.h | 13 +++++++ google/cloud/internal/credentials_impl.cc | 5 +++ google/cloud/internal/credentials_impl.h | 17 +++++++++ .../cloud/internal/credentials_impl_test.cc | 11 ++++++ .../internal/unified_grpc_credentials.cc | 8 ++++ .../internal/unified_grpc_credentials_test.cc | 12 ++++++ .../internal/unified_rest_credentials.cc | 15 ++++++++ .../internal/unified_rest_credentials_test.cc | 38 +++++++++++++++++++ google/cloud/testing_util/credentials.h | 5 +++ 10 files changed, 134 insertions(+) diff --git a/google/cloud/credentials.cc b/google/cloud/credentials.cc index 69a757c62b2ea..0d41b8d327c03 100644 --- a/google/cloud/credentials.cc +++ b/google/cloud/credentials.cc @@ -74,6 +74,16 @@ std::shared_ptr MakeComputeEngineCredentials(Options opts) { std::move(opts)); } +namespace experimental { + +std::shared_ptr MakeUserAccountCredentials(std::string json_object, + Options opts) { + return std::make_shared( + std::move(json_object), std::move(opts)); +} + +} // namespace experimental + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace cloud } // namespace google diff --git a/google/cloud/credentials.h b/google/cloud/credentials.h index 29c884c38d640..ce7f2b36a3ecf 100644 --- a/google/cloud/credentials.h +++ b/google/cloud/credentials.h @@ -28,6 +28,7 @@ namespace google { namespace cloud { GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN +class Credentials; namespace internal { class CredentialsVisitor; } // namespace internal @@ -65,6 +66,18 @@ struct CAInMemoryOption { using Type = std::vector; }; +/** + * Creates user account credentials from a user account JSON key. + * + * User account credentials contain a refresh token, client ID, and client + * secret, typically obtained via OAuth 2.0 authorization flow. + * + * @param json_object the user account configuration as a JSON string. + * @param opts optional configuration values. + */ +std::shared_ptr MakeUserAccountCredentials(std::string json_object, + Options opts = {}); + } // namespace experimental /** diff --git a/google/cloud/internal/credentials_impl.cc b/google/cloud/internal/credentials_impl.cc index f3a4b8ee9e515..6c614f39f888c 100644 --- a/google/cloud/internal/credentials_impl.cc +++ b/google/cloud/internal/credentials_impl.cc @@ -106,6 +106,11 @@ ApiKeyConfig::ApiKeyConfig(std::string api_key, Options opts) ComputeEngineCredentialsConfig::ComputeEngineCredentialsConfig(Options opts) : options_(PopulateAuthOptions(std::move(opts))) {} +AuthorizedUserConfig::AuthorizedUserConfig(std::string json_object, + Options opts) + : json_object_(std::move(json_object)), + options_(PopulateAuthOptions(std::move(opts))) {} + } // namespace internal GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace cloud diff --git a/google/cloud/internal/credentials_impl.h b/google/cloud/internal/credentials_impl.h index 4b7eea36ab21e..4d2ed2d608de0 100644 --- a/google/cloud/internal/credentials_impl.h +++ b/google/cloud/internal/credentials_impl.h @@ -41,6 +41,7 @@ class ServiceAccountConfig; class ExternalAccountConfig; class ApiKeyConfig; class ComputeEngineCredentialsConfig; +class AuthorizedUserConfig; std::shared_ptr MakeErrorCredentials(Status error_status); @@ -56,6 +57,7 @@ class CredentialsVisitor { virtual void visit(ExternalAccountConfig const&) = 0; virtual void visit(ApiKeyConfig const&) = 0; virtual void visit(ComputeEngineCredentialsConfig const&) = 0; + virtual void visit(AuthorizedUserConfig const&) = 0; static void dispatch(Credentials const& credentials, CredentialsVisitor& visitor); @@ -206,6 +208,21 @@ class ComputeEngineCredentialsConfig : public Credentials { Options options_; }; +class AuthorizedUserConfig : public Credentials { + public: + AuthorizedUserConfig(std::string json_object, Options opts); + ~AuthorizedUserConfig() override = default; + + std::string const& json_object() const { return json_object_; } + Options const& options() const { return options_; } + + private: + void dispatch(CredentialsVisitor& v) const override { v.visit(*this); } + + std::string json_object_; + Options options_; +}; + /// A helper function to initialize Auth options. Options PopulateAuthOptions(Options options); diff --git a/google/cloud/internal/credentials_impl_test.cc b/google/cloud/internal/credentials_impl_test.cc index d74aab145cf97..de7fbafed46ea 100644 --- a/google/cloud/internal/credentials_impl_test.cc +++ b/google/cloud/internal/credentials_impl_test.cc @@ -124,6 +124,17 @@ TEST(Credentials, ApiKeyCredentials) { EXPECT_EQ("api-key", visitor.api_key); } +TEST(Credentials, AuthorizedUserCredentials) { + auto credentials = experimental::MakeUserAccountCredentials( + "test-only-invalid", Options{}.set({"scope1", "scope2"})); + TestCredentialsVisitor visitor; + CredentialsVisitor::dispatch(*credentials, visitor); + ASSERT_EQ("AuthorizedUserConfig", visitor.name); + EXPECT_EQ("test-only-invalid", visitor.json_object); + EXPECT_THAT(visitor.options.get(), + ElementsAre("scope1", "scope2")); +} + TEST(PopulateAuthOptions, EmptyOptions) { auto result_options = PopulateAuthOptions(Options{}); diff --git a/google/cloud/internal/unified_grpc_credentials.cc b/google/cloud/internal/unified_grpc_credentials.cc index 45851cce620a7..512ab37ff597b 100644 --- a/google/cloud/internal/unified_grpc_credentials.cc +++ b/google/cloud/internal/unified_grpc_credentials.cc @@ -153,6 +153,14 @@ std::shared_ptr CreateAuthenticationStrategy( void visit(ComputeEngineCredentialsConfig const&) override { result = std::make_unique(options); } + void visit(AuthorizedUserConfig const&) override { + result = std::make_unique( + ErrorCredentialsConfig{UnimplementedError( + "User account credentials (MakeUserAccountCredentials) are not " + "supported with gRPC transport. Use Google Default Credentials " + "or Access Token Credentials instead.", + GCP_ERROR_INFO())}); + } } visitor(std::move(cq), std::move(options)); diff --git a/google/cloud/internal/unified_grpc_credentials_test.cc b/google/cloud/internal/unified_grpc_credentials_test.cc index 4aa2f6ca1e53e..70e5acf5c21ba 100644 --- a/google/cloud/internal/unified_grpc_credentials_test.cc +++ b/google/cloud/internal/unified_grpc_credentials_test.cc @@ -146,6 +146,18 @@ TEST(UnifiedGrpcCredentialsTest, WithApiKeyCredentials) { EXPECT_THAT(headers, Contains(Pair("x-goog-api-key", "api-key"))); } +TEST(UnifiedGrpcCredentialsTest, WithAuthorizedUserCredentials) { + CompletionQueue cq; + auto creds = AuthorizedUserConfig("{}", Options{}); + auto auth = CreateAuthenticationStrategy(creds, cq); + ASSERT_THAT(auth, NotNull()); + EXPECT_TRUE(auth->RequiresConfigureContext()); + + grpc::ClientContext context; + auto configured_context = auth->ConfigureContext(context); + EXPECT_THAT(configured_context, StatusIs(StatusCode::kUnimplemented)); +} + TEST(UnifiedGrpcCredentialsTest, LoadCAInfoNotSet) { auto contents = LoadCAInfo(Options{}); EXPECT_FALSE(contents.has_value()); diff --git a/google/cloud/internal/unified_rest_credentials.cc b/google/cloud/internal/unified_rest_credentials.cc index 596617c57d7d9..57c785ca7f11f 100644 --- a/google/cloud/internal/unified_rest_credentials.cc +++ b/google/cloud/internal/unified_rest_credentials.cc @@ -19,6 +19,7 @@ #include "google/cloud/internal/oauth2_access_token_credentials.h" #include "google/cloud/internal/oauth2_anonymous_credentials.h" #include "google/cloud/internal/oauth2_api_key_credentials.h" +#include "google/cloud/internal/oauth2_authorized_user_credentials.h" #include "google/cloud/internal/oauth2_compute_engine_credentials.h" #include "google/cloud/internal/oauth2_decorate_credentials.h" #include "google/cloud/internal/oauth2_error_credentials.h" @@ -35,6 +36,7 @@ namespace { using ::google::cloud::internal::AccessTokenConfig; using ::google::cloud::internal::ApiKeyConfig; +using ::google::cloud::internal::AuthorizedUserConfig; using ::google::cloud::internal::ComputeEngineCredentialsConfig; using ::google::cloud::internal::CredentialsVisitor; using ::google::cloud::internal::ErrorCredentialsConfig; @@ -153,6 +155,19 @@ std::shared_ptr MapCredentials( Decorate(std::move(creds), std::move(client_factory_), cfg.options()); } + void visit(AuthorizedUserConfig const& cfg) override { + auto info = oauth2_internal::ParseAuthorizedUserCredentials( + cfg.json_object(), "MakeUserAccountCredentials"); + if (!info) { + result = MakeErrorCredentials(std::move(info).status()); + return; + } + auto creds = std::make_shared( + *info, cfg.options(), client_factory_); + result = Decorate(std::move(creds), std::move(client_factory_), + cfg.options()); + } + private: oauth2_internal::HttpClientFactory client_factory_; }; diff --git a/google/cloud/internal/unified_rest_credentials_test.cc b/google/cloud/internal/unified_rest_credentials_test.cc index ab66fa764858d..bbe095c416e74 100644 --- a/google/cloud/internal/unified_rest_credentials_test.cc +++ b/google/cloud/internal/unified_rest_credentials_test.cc @@ -438,6 +438,44 @@ TEST(UnifiedRestCredentialsTest, ServiceAccount) { EXPECT_EQ(access_token->token, *jwt); } +TEST(UnifiedRestCredentialsTest, AuthorizedUser) { + auto const token_uri = std::string{"https://user-refresh.example.com"}; + auto const contents = nlohmann::json{ + {"client_id", "a-client-id.example.com"}, + {"client_secret", "a-123456ABCDEF"}, + {"refresh_token", "1/THETOKEN"}, + {"type", "authorized_user"}, + {"token_uri", token_uri}, + }; + + auto const now = std::chrono::system_clock::now(); + + MockClientFactory client_factory; + EXPECT_CALL(client_factory, Call).WillOnce([token_uri]() { + auto client = std::make_unique(); + using FormDataType = std::vector>; + auto expected_request = Property(&RestRequest::path, token_uri); + auto expected_form_data = MatcherCast(IsSupersetOf({ + Pair("grant_type", "refresh_token"), + Pair("client_id", "a-client-id.example.com"), + Pair("client_secret", "a-123456ABCDEF"), + Pair("refresh_token", "1/THETOKEN"), + })); + EXPECT_CALL(*client, Post(_, expected_request, expected_form_data)) + .WillOnce(Return( + Status{StatusCode::kPermissionDenied, "uh-oh - user refresh"})); + return client; + }); + + auto const config = + internal::AuthorizedUserConfig(contents.dump(), Options{}); + auto credentials = MapCredentials(config, client_factory.AsStdFunction()); + + auto access_token = credentials->GetToken(now); + EXPECT_THAT(access_token, + StatusIs(StatusCode::kPermissionDenied, "uh-oh - user refresh")); +} + TEST(UnifiedRestCredentialsTest, ExternalAccount) { // This sets up a mocked request for the subject token. auto const subject_url = std::string{"https://test-only-oidc.example.com/"}; diff --git a/google/cloud/testing_util/credentials.h b/google/cloud/testing_util/credentials.h index 1aca048ccb02f..df0b8d88b9c76 100644 --- a/google/cloud/testing_util/credentials.h +++ b/google/cloud/testing_util/credentials.h @@ -72,6 +72,11 @@ struct TestCredentialsVisitor : public internal::CredentialsVisitor { name = "ComputeEngineCredentialsConfig"; options = cfg.options(); } + void visit(internal::AuthorizedUserConfig const& cfg) override { + name = "AuthorizedUserConfig"; + json_object = cfg.json_object(); + options = cfg.options(); + } }; } // namespace testing_util