Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions google/cloud/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ cc_library(
name = "google_cloud_cpp_rest_internal",
srcs = google_cloud_cpp_rest_internal_srcs,
hdrs = google_cloud_cpp_rest_internal_hdrs,
# TODO(#16079): Remove macro definition when GA.
cxxopts = ["-DGOOGLE_CLOUD_CPP_TESTING_ENABLE_RAB"],
linkopts = select({
"@platforms//os:windows": [
"-DEFAULTLIB:bcrypt.lib",
Expand Down Expand Up @@ -273,6 +275,8 @@ cc_library(
[cc_test(
name = test.replace("/", "_").replace(".cc", ""),
srcs = [test],
# TODO(#16079): Remove macro definition when GA.
cxxopts = ["-DGOOGLE_CLOUD_CPP_TESTING_ENABLE_RAB"],
deps = [
":google_cloud_cpp_rest_internal",
"//google/cloud/testing_util:google_cloud_cpp_testing_private",
Expand Down
9 changes: 3 additions & 6 deletions google/cloud/internal/oauth2_api_key_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,9 @@ StatusOr<AccessToken> ApiKeyCredentials::GetToken(
return AccessToken{std::string{}, tp};
}

StatusOr<std::vector<rest_internal::HttpHeader>>
ApiKeyCredentials::AuthenticationHeaders(std::chrono::system_clock::time_point,
std::string_view) {
std::vector<rest_internal::HttpHeader> headers;
headers.emplace_back("x-goog-api-key", api_key_);
return headers;
StatusOr<rest_internal::HttpHeader> ApiKeyCredentials::Authorization(
std::chrono::system_clock::time_point) {
return rest_internal::HttpHeader{"x-goog-api-key", api_key_};
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
5 changes: 2 additions & 3 deletions google/cloud/internal/oauth2_api_key_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ class ApiKeyCredentials : public oauth2_internal::Credentials {
StatusOr<AccessToken> GetToken(
std::chrono::system_clock::time_point tp) override;

StatusOr<std::vector<rest_internal::HttpHeader>> AuthenticationHeaders(
std::chrono::system_clock::time_point,
std::string_view endpoint) override;
StatusOr<rest_internal::HttpHeader> Authorization(
std::chrono::system_clock::time_point tp) override;

private:
std::string api_key_;
Expand Down
5 changes: 5 additions & 0 deletions google/cloud/internal/oauth2_cached_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ StatusOr<std::string> CachedCredentials::project_id(
return impl_->project_id(options);
}

Credentials::AllowedLocationsRequestType
CachedCredentials::AllowedLocationsRequest() const {
return impl_->AllowedLocationsRequest();
}

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
} // namespace oauth2_internal
} // namespace cloud
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/internal/oauth2_cached_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class CachedCredentials : public Credentials {
StatusOr<std::string> universe_domain(Options const& options) const override;
StatusOr<std::string> project_id() const override;
StatusOr<std::string> project_id(Options const& options) const override;
Credentials::AllowedLocationsRequestType AllowedLocationsRequest()
const override;

private:
std::shared_ptr<Credentials> impl_;
Expand Down
10 changes: 10 additions & 0 deletions google/cloud/internal/oauth2_compute_engine_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ StatusOr<std::string> ComputeEngineCredentials::project_id(
return RetrieveProjectId(lk, options);
}

Credentials::AllowedLocationsRequestType
ComputeEngineCredentials::AllowedLocationsRequest() const {
// TODO(#16079): Remove conditional and else clause when GA.
#ifdef GOOGLE_CLOUD_CPP_TESTING_ENABLE_RAB
return ServiceAccountAllowedLocationsRequest{AccountEmail()};
#else
return std::monostate{};
#endif
}

StatusOr<std::string> ComputeEngineCredentials::RetrieveUniverseDomain(
std::lock_guard<std::mutex> const&, Options const& options) const {
// Fetch the universe domain only once.
Expand Down
2 changes: 2 additions & 0 deletions google/cloud/internal/oauth2_compute_engine_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ class ComputeEngineCredentials : public Credentials {
StatusOr<std::string> project_id(
google::cloud::Options const& options) const override;

AllowedLocationsRequestType AllowedLocationsRequest() const override;

/**
* Returns the email or alias of this credential's service account.
*
Expand Down
14 changes: 14 additions & 0 deletions google/cloud/internal/oauth2_compute_engine_credentials_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ using ::testing::Property;
using ::testing::Return;
using ::testing::UnorderedElementsAre;
using ::testing::UnorderedElementsAreArray;
using ::testing::VariantWith;

using MockHttpClientFactory =
::testing::MockFunction<std::unique_ptr<rest_internal::RestClient>(
Expand Down Expand Up @@ -387,6 +388,10 @@ TEST(ComputeEngineCredentialsTest, FailedRefresh) {
HasSubstr("Could not find all required fields")));
}

MATCHER_P(RequestServiceAccountEmailIs, email, "has service account email") {
return email == arg.service_account_email;
}

/// @test Verify that we can force a refresh of the service account email.
TEST(ComputeEngineCredentialsTest, AccountEmail) {
auto const alias = std::string{"default"};
Expand Down Expand Up @@ -416,6 +421,15 @@ TEST(ComputeEngineCredentialsTest, AccountEmail) {
auto refreshed_email = credentials.AccountEmail();
EXPECT_EQ(email, refreshed_email);
EXPECT_EQ(credentials.service_account_email(), refreshed_email);
// TODO(#16079): Remove conditional and else clause when GA.
#ifdef GOOGLE_CLOUD_CPP_TESTING_ENABLE_RAB
EXPECT_THAT(credentials.AllowedLocationsRequest(),
VariantWith<ServiceAccountAllowedLocationsRequest>(
RequestServiceAccountEmailIs(email)));
#else
EXPECT_THAT(credentials.AllowedLocationsRequest(),
VariantWith<std::monostate>(std::monostate()));
#endif
}

auto expected_universe_domain_request = []() {
Expand Down
5 changes: 3 additions & 2 deletions google/cloud/internal/oauth2_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ Credentials::AuthenticationHeaders(std::chrono::system_clock::time_point tp,
if (!authorization->empty()) headers.push_back(*std::move(authorization));

auto allowed_locations = AllowedLocations(tp, endpoint);
// Not all credential types support the x-allowed-locations header. For those
// that do, if there is a problem retrieving the header, omit the header.
// Not all credential types support the x-allowed-locations header. For
// those that do, if there is a problem retrieving the header, omit the
// header.
if (allowed_locations.ok() && !allowed_locations->empty()) {
headers.push_back(*std::move(allowed_locations));
}
Expand Down
33 changes: 30 additions & 3 deletions google/cloud/internal/oauth2_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,27 @@
#include "google/cloud/version.h"
#include <chrono>
#include <string>
#include <variant>
#include <vector>

namespace google {
namespace cloud {
namespace oauth2_internal {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN

struct ServiceAccountAllowedLocationsRequest {
std::string service_account_email;
};

struct WorkloadIdentityAllowedLocationsRequest {
std::string project_id;
std::string pool_id;
};

struct WorkforceIdentityAllowedLocationsRequest {
std::string pool_id;
};

/**
* Interface for OAuth 2.0 credentials for use with Google's Unified Auth Client
* (GUAC) library. Internally, GUAC credentials are mapped to the appropriate
Expand Down Expand Up @@ -69,9 +83,8 @@ class Credentials {
* @param endpoint the endpoint of the GCP service the RPC request will be
* sent to.
*/
virtual StatusOr<std::vector<rest_internal::HttpHeader>>
AuthenticationHeaders(std::chrono::system_clock::time_point tp,
std::string_view endpoint);
StatusOr<std::vector<rest_internal::HttpHeader>> AuthenticationHeaders(
std::chrono::system_clock::time_point tp, std::string_view endpoint);
Comment thread
scotthart marked this conversation as resolved.

/**
* Try to sign @p string_to_sign using @p service_account.
Expand Down Expand Up @@ -160,6 +173,20 @@ class Credentials {
*/
virtual StatusOr<AccessToken> GetToken(
std::chrono::system_clock::time_point tp) = 0;

using AllowedLocationsRequestType =
std::variant<std::monostate, ServiceAccountAllowedLocationsRequest,
WorkforceIdentityAllowedLocationsRequest,
WorkloadIdentityAllowedLocationsRequest>;
/**
* Obtains the request type from the underlying credential, if supported.
*
* Not all credential types support the `x-allowed-locations` header, but
* those that do vary in the data needed to format the request to IAM.
*/
virtual AllowedLocationsRequestType AllowedLocationsRequest() const {
return std::monostate{};
}
};

GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
Expand Down
46 changes: 44 additions & 2 deletions google/cloud/internal/oauth2_external_account_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "google/cloud/internal/rest_client.h"
#include "absl/strings/str_cat.h"
#include <nlohmann/json.hpp>
#include <regex>

namespace google {
namespace cloud {
Expand All @@ -49,8 +50,28 @@ StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSource(
GCP_ERROR_INFO().WithContext(ec));
}

absl::optional<WorkloadIdentityFederationInfo> WorkloadIdentityFromAudience(
std::string const& audience) {
auto constexpr kPattern =
R"""(iam.googleapis.com/projects/([^/]+)/locations/global/workloadIdentityPools/([^/]+)/)""";
static auto* re = new std::regex{kPattern, std::regex::optimize};
Comment thread
scotthart marked this conversation as resolved.
Outdated
std::smatch match;
if (!std::regex_search(audience, match, *re)) {
return absl::nullopt;
}
return WorkloadIdentityFederationInfo{match[1], match[2]};
}

} // namespace

bool ExternalAccountInfo::IsWorkforceIdentityFederation() const {
return workforce_pool_user_project.has_value();
}

bool ExternalAccountInfo::IsWorkloadIdentityFederation() const {
return workload_info.has_value();
}

/// Parse a JSON string with an external account configuration.
StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(
std::string const& configuration, internal::ErrorContext const& ec) {
Expand All @@ -70,6 +91,10 @@ StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(

auto audience = ValidateStringField(json, "audience", "credentials-file", ec);
if (!audience) return std::move(audience).status();

// extract workload project_number and pool_id from audience, if it exists
auto workload_identity = WorkloadIdentityFromAudience(*audience);

auto subject_token_type =
ValidateStringField(json, "subject_token_type", "credentials-file", ec);
if (!subject_token_type) return std::move(subject_token_type).status();
Expand Down Expand Up @@ -108,7 +133,8 @@ StatusOr<ExternalAccountInfo> ParseExternalAccountConfiguration(
*std::move(source),
absl::nullopt,
*std::move(universe_domain),
std::move(workforce_pool_user_project)};
std::move(workforce_pool_user_project),
std::move(workload_identity)};

it = json.find("service_account_impersonation_url");
if (it == json.end()) return info;
Expand Down Expand Up @@ -161,7 +187,7 @@ StatusOr<AccessToken> ExternalAccountCredentials::GetToken(
// Workforce Identity is handled at the org level and requires the userProject
// header. Workload Identity is handled at the project level and doesn't
// require the header.
if (info_.workforce_pool_user_project) {
if (info_.IsWorkforceIdentityFederation()) {
form_data.emplace_back(
"options", absl::StrCat(R"({"userProject": ")",
*info_.workforce_pool_user_project, R"("})"));
Comment thread
scotthart marked this conversation as resolved.
Expand Down Expand Up @@ -221,6 +247,22 @@ StatusOr<AccessToken> ExternalAccountCredentials::GetToken(
return AccessToken{*token, tp + std::chrono::seconds(*expires_in)};
}

Credentials::AllowedLocationsRequestType
ExternalAccountCredentials::AllowedLocationsRequest() const {
Credentials::AllowedLocationsRequestType request = std::monostate{};
// TODO(#16079): Remove conditional and else clause when GA.
#ifdef GOOGLE_CLOUD_CPP_TESTING_ENABLE_RAB
if (info_.IsWorkforceIdentityFederation()) {
request = WorkforceIdentityAllowedLocationsRequest{
*info_.workforce_pool_user_project};
Comment thread
scotthart marked this conversation as resolved.
Outdated
} else if (info_.IsWorkloadIdentityFederation()) {
request = WorkloadIdentityAllowedLocationsRequest{
info_.workload_info->project_id, info_.workload_info->pool_id};
}
#endif
return request;
}

StatusOr<AccessToken> ExternalAccountCredentials::GetTokenImpersonation(
std::string const& access_token, internal::ErrorContext const& ec) {
auto request = rest_internal::RestRequest(info_.impersonation_config->url);
Expand Down
10 changes: 10 additions & 0 deletions google/cloud/internal/oauth2_external_account_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ struct ExternalAccountImpersonationConfig {
std::chrono::seconds token_lifetime;
};

struct WorkloadIdentityFederationInfo {
std::string project_id;
std::string pool_id;
};

/**
* An external account configuration.
*
Expand All @@ -69,6 +74,9 @@ struct ExternalAccountInfo {
absl::optional<ExternalAccountImpersonationConfig> impersonation_config;
std::string universe_domain;
absl::optional<std::string> workforce_pool_user_project;
absl::optional<WorkloadIdentityFederationInfo> workload_info;
bool IsWorkforceIdentityFederation() const;
bool IsWorkloadIdentityFederation() const;
};

/// Parse a JSON string with an external account configuration.
Expand All @@ -89,6 +97,8 @@ class ExternalAccountCredentials : public oauth2_internal::Credentials {
return info_.universe_domain;
}

AllowedLocationsRequestType AllowedLocationsRequest() const override;

private:
StatusOr<AccessToken> GetTokenImpersonation(std::string const& access_token,
internal::ErrorContext const& ec);
Expand Down
Loading
Loading