Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
61 changes: 59 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,41 @@ StatusOr<ExternalAccountTokenSource> MakeExternalAccountTokenSource(
GCP_ERROR_INFO().WithContext(ec));
}

std::variant<std::monostate, WorkforceIdentityFederationInfo,
WorkloadIdentityFederationInfo>
IdentityFederationFromAudience(std::string const& audience) {
auto constexpr kWorkloadPattern =
R"""(iam.googleapis.com/projects/([^/]+)/locations/global/workloadIdentityPools/([^/]+)/)""";
static auto* workload_re =
new std::regex{kWorkloadPattern, std::regex::optimize};

auto constexpr kWorkforcePattern =
R"""(iam.googleapis.com/locations/global/workforcePools/([^/]+)/)""";
static auto* workforce_re =
new std::regex{kWorkforcePattern, std::regex::optimize};

std::smatch match;
if (std::regex_search(audience, match, *workload_re)) {
return WorkloadIdentityFederationInfo{match[1], match[2]};
}
if (std::regex_search(audience, match, *workforce_re)) {
return WorkforceIdentityFederationInfo{match[1]};
}
return std::monostate{};
}

} // namespace

bool ExternalAccountInfo::IsWorkforceIdentityFederation() const {
return std::holds_alternative<WorkforceIdentityFederationInfo>(
identity_federation_info);
}

bool ExternalAccountInfo::IsWorkloadIdentityFederation() const {
return std::holds_alternative<WorkloadIdentityFederationInfo>(
identity_federation_info);
}

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

auto audience = ValidateStringField(json, "audience", "credentials-file", ec);
if (!audience) return std::move(audience).status();
auto identity_federation = IdentityFederationFromAudience(*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 +144,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(identity_federation)};

it = json.find("service_account_impersonation_url");
if (it == json.end()) return info;
Expand Down Expand Up @@ -161,7 +198,8 @@ 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() &&
info_.workforce_pool_user_project.has_value()) {
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 +259,25 @@ 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()) {
auto wif = std::get<WorkforceIdentityFederationInfo>(
info_.identity_federation_info);
request = WorkforceIdentityAllowedLocationsRequest{wif.pool_id};
} else if (info_.IsWorkloadIdentityFederation()) {
auto wif = std::get<WorkloadIdentityFederationInfo>(
info_.identity_federation_info);
request =
WorkloadIdentityAllowedLocationsRequest{wif.project_id, wif.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
16 changes: 16 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,15 @@ struct ExternalAccountImpersonationConfig {
std::chrono::seconds token_lifetime;
};

struct WorkforceIdentityFederationInfo {
std::string pool_id;
};

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

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

/// Parse a JSON string with an external account configuration.
Expand All @@ -89,6 +103,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