Skip to content

Commit

Permalink
feat(rest): support impersonated ADC
Browse files Browse the repository at this point in the history
  • Loading branch information
dbolduc committed Oct 31, 2024
1 parent a41a136 commit e925c95
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 0 deletions.
26 changes: 26 additions & 0 deletions google/cloud/internal/oauth2_google_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/oauth2_google_application_default_credentials_file.h"
#include "google/cloud/internal/oauth2_http_client_factory.h"
#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h"
#include "google/cloud/internal/oauth2_service_account_credentials.h"
#include "google/cloud/internal/parse_service_account_p12_file.h"
#include "google/cloud/internal/throw_delegate.h"
Expand All @@ -36,6 +37,7 @@ namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace {

// NOLINTNEXTLINE(misc-no-recursion)
StatusOr<std::unique_ptr<Credentials>> LoadCredsFromString(
std::string const& contents, std::string const& path,
Options const& options, HttpClientFactory client_factory) {
Expand Down Expand Up @@ -65,6 +67,30 @@ StatusOr<std::unique_ptr<Credentials>> LoadCredsFromString(
std::make_unique<ServiceAccountCredentials>(*info, options,
std::move(client_factory)));
}
if (cred_type == "impersonated_service_account") {
auto info = ParseImpersonatedServiceAccountCredentials(contents, path);
if (!info) return std::move(info).status();
auto source_creds = LoadCredsFromString(std::move(info->source_credentials),
path, options, client_factory);
if (!source_creds) return std::move(source_creds).status();

auto opts = options;
auto& delegates = opts.lookup<DelegatesOption>();
for (auto& delegate : info->delegates) {
delegates.push_back(std::move(delegate));
}

internal::ImpersonateServiceAccountConfig config(
// The base credentials (GUAC) are used to create the IAM REST Stub. We
// are going to override them by supplying our own IAM REST Stub,
// constructed using `oauth2_internal::Credentials`.
nullptr, std::move(info->service_account), opts);
auto rest_stub = MakeMinimalIamCredentialsRestStub(
*std::move(source_creds), opts, std::move(client_factory));
return std::unique_ptr<Credentials>(
std::make_unique<ImpersonateServiceAccountCredentials>(
config, std::move(rest_stub)));
}
return internal::InvalidArgumentError(
"Unsupported credential type (" + cred_type +
") when reading Application Default Credentials file "
Expand Down
107 changes: 107 additions & 0 deletions google/cloud/internal/oauth2_google_credentials_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "google/cloud/internal/oauth2_compute_engine_credentials.h"
#include "google/cloud/internal/oauth2_external_account_credentials.h"
#include "google/cloud/internal/oauth2_google_application_default_credentials_file.h"
#include "google/cloud/internal/oauth2_impersonate_service_account_credentials.h"
#include "google/cloud/internal/oauth2_service_account_credentials.h"
#include "google/cloud/internal/random.h"
#include "google/cloud/testing_util/mock_rest_client.h"
Expand Down Expand Up @@ -91,6 +92,21 @@ auto constexpr kExternalAccountContents = R"""({
"credential_source": {"url": "https://subject.example.com/"}
})""";

auto constexpr kImpersonatedServiceAccountContents = R"""({
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/sa3@developer.gserviceaccount.com:generateAccessToken",
"delegates": [
"sa1@developer.gserviceaccount.com",
"sa2@developer.gserviceaccount.com"
],
"source_credentials": {
"client_id": "test-invalid-test-invalid.apps.googleusercontent.com",
"client_secret": "invalid-invalid-invalid",
"refresh_token": "1/test-test-test",
"type": "authorized_user"
},
"type": "impersonated_service_account"
})""";

std::string TempFileName() {
static auto generator =
google::cloud::internal::DefaultPRNG(std::random_device{}());
Expand Down Expand Up @@ -227,6 +243,45 @@ TEST_F(GoogleCredentialsTest, LoadValidServiceAccountCredentialsViaGcloudFile) {
WhenDynamicCastTo<ServiceAccountCredentials*>(NotNull()));
}

TEST_F(GoogleCredentialsTest,
LoadValidImpersonatedServiceAccountCredentialsViaEnvVar) {
auto const filename = TempFileName();
std::ofstream(filename) << kImpersonatedServiceAccountContents;
auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str());

// Test that the impersonated service account credentials are loaded as the
// default when specified via the well-known environment variable.
MockHttpClientFactory client_factory;
EXPECT_CALL(client_factory, Call).Times(0);
auto creds =
GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction());
(void)std::remove(filename.c_str());
ASSERT_STATUS_OK(creds);
EXPECT_THAT(
creds->get(),
WhenDynamicCastTo<ImpersonateServiceAccountCredentials*>(NotNull()));
}

TEST_F(GoogleCredentialsTest,
LoadValidImpersonatedServiceAccountCredentialsViaGcloudFile) {
auto const filename = TempFileName();
std::ofstream(filename) << kImpersonatedServiceAccountContents;
auto const env =
ScopedEnvironment(GoogleGcloudAdcFileEnvVar(), filename.c_str());

// Test that the impersonated service account credentials are loaded as the
// default when specified via the well-known environment variable.
MockHttpClientFactory client_factory;
EXPECT_CALL(client_factory, Call).Times(0);
auto creds =
GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction());
(void)std::remove(filename.c_str());
ASSERT_STATUS_OK(creds);
EXPECT_THAT(
creds->get(),
WhenDynamicCastTo<ImpersonateServiceAccountCredentials*>(NotNull()));
}

TEST_F(GoogleCredentialsTest, LoadComputeEngineCredentialsFromADCFlow) {
// Developers may have an ADC file in $HOME/.gcloud, override the default
// path to a location that is not going to succeed.
Expand Down Expand Up @@ -298,6 +353,58 @@ TEST_F(GoogleCredentialsTest, LoadInvalidServiceAccountCredentialsViaADC) {
(void)std::remove(filename.c_str());
}

TEST_F(GoogleCredentialsTest,
LoadInvalidImpersonatedServiceAccountCredentials) {
auto const filename = TempFileName();
std::ofstream(filename) << R"""({"type": "impersonated_service_account"})""";
auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str());

MockHttpClientFactory client_factory;
EXPECT_CALL(client_factory, Call).Times(0);
auto creds =
GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction());
EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument));
(void)std::remove(filename.c_str());
}

TEST_F(GoogleCredentialsTest,
LoadImpersonatedServiceAccountCredentialsWithInvalidPathUrl) {
auto const filename = TempFileName();
std::ofstream(filename) << R"""({
"service_account_impersonation_url": "invalid-url",
"source_credentials": {},
"type": "impersonated_service_account"
})""";
auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str());

MockHttpClientFactory client_factory;
EXPECT_CALL(client_factory, Call).Times(0);
auto creds =
GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction());
EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument));
(void)std::remove(filename.c_str());
}

TEST_F(GoogleCredentialsTest,
LoadImpersonatedServiceAccountWithInvalidSourceCredentials) {
auto const filename = TempFileName();
std::ofstream(filename) << R"""({
"service_account_impersonation_url": "invalid-string",
"source_credentials": {
"type": "user_account"
},
"type": "impersonated_service_account"
})""";
auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename.c_str());

MockHttpClientFactory client_factory;
EXPECT_CALL(client_factory, Call).Times(0);
auto creds =
GoogleDefaultCredentials(Options{}, client_factory.AsStdFunction());
EXPECT_THAT(creds, StatusIs(StatusCode::kInvalidArgument));
(void)std::remove(filename.c_str());
}

TEST_F(GoogleCredentialsTest, MissingCredentialsViaEnvVar) {
auto const filename = TempFileName();
auto const env = ScopedEnvironment(GoogleAdcEnvVar(), filename);
Expand Down

0 comments on commit e925c95

Please sign in to comment.