Skip to content
Open
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
182 changes: 172 additions & 10 deletions google/cloud/internal/unified_grpc_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@
#include "google/cloud/internal/unified_grpc_credentials.h"
#include "google/cloud/grpc_error_delegate.h"
#include "google/cloud/grpc_options.h"
#include "google/cloud/internal/filesystem.h"
#include "google/cloud/internal/getenv.h"
#include "google/cloud/internal/grpc_access_token_authentication.h"
#include "google/cloud/internal/grpc_api_key_authentication.h"
#include "google/cloud/internal/grpc_channel_credentials_authentication.h"
#include "google/cloud/internal/grpc_compute_engine_authentication.h"
#include "google/cloud/internal/grpc_impersonate_service_account.h"
#include "google/cloud/internal/grpc_service_account_authentication.h"
#include "google/cloud/internal/make_status.h"
#include "absl/strings/str_cat.h"
#include <grpcpp/security/credentials.h>
#if __has_include(<grpcpp/version_info.h>)
#include <grpcpp/version_info.h>
#endif
#include <nlohmann/json.hpp>
#include <fstream>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>

namespace {

Expand Down Expand Up @@ -78,6 +86,159 @@ class GrpcErrorCredentialsAuthentication : public GrpcAuthenticationStrategy {
Status error_status_;
};

namespace {

// The lookup below mirrors `oauth2_internal::GoogleDefaultCredentials()`. The
// REST helpers it would reuse are compiled into `rest_internal`, which depends
// on libcurl; `grpc_utils` does not link that library, so the lookup is
// duplicated here rather than adding the dependency.
std::string AdcFilePathFromEnvVarOrEmpty() {
return GetEnv("GOOGLE_APPLICATION_CREDENTIALS").value_or("");
}

std::string AdcFilePathFromWellKnownPathOrEmpty() {
std::optional<std::string> const override_path =
GetEnv("GOOGLE_GCLOUD_ADC_PATH_OVERRIDE");
if (override_path.has_value()) return *override_path;
#ifdef _WIN32
std::optional<std::string> const root = GetEnv("APPDATA");
constexpr std::string_view kSuffix =
"/gcloud/application_default_credentials.json";
#else
std::optional<std::string> const root = GetEnv("HOME");
constexpr std::string_view kSuffix =
"/.config/gcloud/application_default_credentials.json";
#endif
if (!root.has_value()) return "";
return absl::StrCat(*root, kSuffix);
}

// gRPC's Google Default credentials accept a fixed set of `type` values; each
// of its credential factories checks the field again and returns null for
// anything else.
AdcLookup ClassifyAdcFile(std::string path, std::string contents) {
AdcLookup lookup;
lookup.path = std::move(path);
lookup.contents = std::move(contents);
nlohmann::json const json =
nlohmann::json::parse(*lookup.contents, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
lookup.source = AdcSource::kNotJsonObject;
return lookup;
}
auto const type = json.find("type");
if (type != json.end() && type->is_string()) {
lookup.type = type->get<std::string>();
}
if (lookup.type == "service_account") {
lookup.source = AdcSource::kServiceAccount;
} else if (lookup.type == "authorized_user") {
lookup.source = AdcSource::kAuthorizedUser;
} else if (lookup.type == "external_account") {
lookup.source = AdcSource::kExternalAccount;
} else {
lookup.source = AdcSource::kUnsupportedType;
}
return lookup;
}

std::shared_ptr<GrpcAuthenticationStrategy> ErrorStrategy(Status status) {
return std::make_shared<GrpcErrorCredentialsAuthentication>(
ErrorCredentialsConfig{std::move(status)});
}

std::shared_ptr<grpc::ChannelCredentials> MakeSslCredentials(
Options const& options) {
grpc::SslCredentialsOptions ssl_options;
std::optional<std::string> cainfo = LoadCAInfo(options);
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
return grpc::SslCredentials(ssl_options);
}

// gRPC's call-credential factories return nullptr on malformed input, and
// `grpc::CompositeChannelCredentials()` dereferences its argument without
// checking for null, so we validate @p call_creds first. @p factory names the
// gRPC factory for the error message.
std::shared_ptr<GrpcAuthenticationStrategy> TlsWithCallCredentials(
Options const& options,
std::shared_ptr<grpc::CallCredentials> const& call_creds,
Comment thread
kalragauri marked this conversation as resolved.
std::string_view factory, std::optional<std::string> const& path) {
if (!call_creds) {
return ErrorStrategy(InvalidArgumentError(
absl::StrCat("Error creating ", factory,
" for Application Default Credentials",
path ? absl::StrCat(" from ", *path) : ""),
GCP_ERROR_INFO()));
}
return std::make_shared<GrpcChannelCredentialsAuthentication>(
grpc::CompositeChannelCredentials(MakeSslCredentials(options),
call_creds));
}

// The strategy for `GrpcGoogleDefaultTlsOnlyOption`: the call credentials
// gRPC's `GoogleDefaultCredentials()` derives from Application Default
// Credentials (see `create_default_creds_from_path()` in gRPC), composed with
// TLS instead of letting gRPC choose between TLS and ALTS per endpoint. The
// error codes and messages follow the REST ADC loader.
std::shared_ptr<GrpcAuthenticationStrategy> MakeTlsGoogleDefaultStrategy(
GoogleDefaultCredentialsConfig const& cfg, Options const& options) {
StatusOr<AdcLookup> adc = LookupApplicationDefaultCredentials();
if (!adc) return ErrorStrategy(std::move(adc).status());
switch (adc->source) {
case AdcSource::kNone:
return TlsWithCallCredentials(
options, grpc::GoogleComputeEngineCredentials(),
"grpc::GoogleComputeEngineCredentials", adc->path);
case AdcSource::kServiceAccount:
return TlsWithCallCredentials(
options, grpc::ServiceAccountJWTAccessCredentials(*adc->contents),
"grpc::ServiceAccountJWTAccessCredentials", adc->path);
case AdcSource::kAuthorizedUser:
return TlsWithCallCredentials(
options, grpc::GoogleRefreshTokenCredentials(*adc->contents),
"grpc::GoogleRefreshTokenCredentials", adc->path);
case AdcSource::kExternalAccount:
return TlsWithCallCredentials(
options,
GrpcExternalAccountCredentials(
ExternalAccountConfig(*adc->contents, cfg.options())),
"grpc::ExternalAccountCredentials", adc->path);
case AdcSource::kNotJsonObject:
return ErrorStrategy(InvalidArgumentError(
absl::StrCat("Cannot open credentials file ", *adc->path,
", it does not contain a JSON object."),
GCP_ERROR_INFO()));
case AdcSource::kUnsupportedType:
break;
}
return ErrorStrategy(InvalidArgumentError(
absl::StrCat("Unsupported credential type (",
adc->type.value_or("no type given"),
") when reading Application Default Credentials file from ",
*adc->path, "."),
GCP_ERROR_INFO()));
}

} // namespace

StatusOr<AdcLookup> LookupApplicationDefaultCredentials() {
std::string path = AdcFilePathFromEnvVarOrEmpty();
if (path.empty()) {
path = AdcFilePathFromWellKnownPathOrEmpty();
std::error_code ec;
if (path.empty() || !exists(status(path, ec))) return AdcLookup{};
}
std::ifstream is(path);
if (!is.is_open()) {
// We use kUnknown here because we don't know if the file does not exist,
// or if we were unable to open it for some other reason.
return UnknownError("Cannot open credentials file " + path,
GCP_ERROR_INFO());
}
std::string contents(std::istreambuf_iterator<char>{is}, {});
return ClassifyAdcFile(std::move(path), std::move(contents));
}

std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
google::cloud::CompletionQueue cq, Options const& options) {
if (options.has<google::cloud::UnifiedCredentialsOption>()) {
Expand Down Expand Up @@ -106,7 +267,11 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::InsecureChannelCredentials());
}
void visit(GoogleDefaultCredentialsConfig const&) override {
void visit(GoogleDefaultCredentialsConfig const& cfg) override {
if (options.get<GrpcGoogleDefaultTlsOnlyOption>()) {
result = MakeTlsGoogleDefaultStrategy(cfg, options);
return;
}
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::GoogleDefaultCredentials());
}
Expand Down Expand Up @@ -151,11 +316,8 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
GCP_ERROR_INFO())});
return;
}
grpc::SslCredentialsOptions ssl_options;
auto cainfo = LoadCAInfo(options);
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::CompositeChannelCredentials(grpc::SslCredentials(ssl_options),
grpc::CompositeChannelCredentials(MakeSslCredentials(options),
std::move(external_account_creds)));
}
void visit(ApiKeyConfig const& cfg) override {
Expand Down Expand Up @@ -216,7 +378,7 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
ca_cert_path = it->get<std::string>();
}
}
grpc::SslCredentialsOptions ssl_options;
std::shared_ptr<grpc::ChannelCredentials> ssl_creds;
if (!ca_cert_path.empty()) {
std::ifstream is(ca_cert_path);
if (!is.is_open()) {
Expand All @@ -226,15 +388,15 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
GCP_ERROR_INFO())});
return;
}
grpc::SslCredentialsOptions ssl_options;
ssl_options.pem_root_certs =
std::string{std::istreambuf_iterator<char>{is.rdbuf()}, {}};
ssl_creds = grpc::SslCredentials(ssl_options);
} else {
std::optional<std::string> cainfo = LoadCAInfo(options);
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
ssl_creds = MakeSslCredentials(options);
}
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::CompositeChannelCredentials(grpc::SslCredentials(ssl_options),
gdch_creds));
grpc::CompositeChannelCredentials(ssl_creds, gdch_creds));
#else
(void)cfg;
result = std::make_unique<GrpcErrorCredentialsAuthentication>(
Expand Down
54 changes: 54 additions & 0 deletions google/cloud/internal/unified_grpc_credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,70 @@
#include "google/cloud/credentials.h"
#include "google/cloud/future.h"
#include "google/cloud/status.h"
#include "google/cloud/status_or.h"
#include "google/cloud/version.h"
#include <grpcpp/grpcpp.h>
#include <memory>
#include <optional>
#include <string>

namespace google {
namespace cloud {
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
namespace internal {

/**
* Use TLS channel credentials for `MakeGoogleDefaultCredentials()`.
*
* `grpc::GoogleDefaultCredentials()` selects ALTS for the endpoints of xDS
* clusters that are not fronted by Google's edge proxies. Some such clusters,
* for example DirectPath over Interconnect, serve hosts outside Google Cloud,
* where ALTS is unavailable. With this option the authentication strategy for
* Google Default credentials composes `grpc::SslCredentials()` with the call
* credentials that Application Default Credentials resolve to, attached at the
* channel level so that gRPC's control-plane channels (such as RLS) inherit
* them.
*
* Libraries set this option for endpoints known to require TLS; it is not
* intended for applications.
*/
struct GrpcGoogleDefaultTlsOnlyOption {
using Type = bool;
};

/**
* The outcome of the Application Default Credentials file lookup, classified
* the way gRPC's Google Default credentials classify it.
*/
enum class AdcSource {
kNone, ///< No file found; use Compute Engine credentials.
kServiceAccount,
kAuthorizedUser,
kExternalAccount,
kNotJsonObject, ///< The file does not contain a JSON object.
kUnsupportedType, ///< A `type` gRPC's Google Default credentials reject.
};

/// The parsed result of `LookupApplicationDefaultCredentials()`.
struct AdcLookup {
AdcSource source = AdcSource::kNone;
std::optional<std::string> path; ///< Set unless `source` is `kNone`.
std::optional<std::string> contents; ///< Set unless `source` is `kNone`.
std::optional<std::string> type; ///< The JSON `type`, if it is a string.
};

/**
* Finds and reads the Application Default Credentials file.
*
* Uses the same search order as the REST credentials:
* `GOOGLE_APPLICATION_CREDENTIALS`, then the gcloud file (the
* `GOOGLE_GCLOUD_ADC_PATH_OVERRIDE` path or the well-known location), which
* is used only if it exists. gRPC's Google Default credentials search the same
* locations, minus the override. Returns an error only when a configured file
* cannot be opened; every other outcome is described by `AdcLookup::source`.
*/
StatusOr<AdcLookup> LookupApplicationDefaultCredentials();

class GrpcAuthenticationStrategy {
public:
virtual ~GrpcAuthenticationStrategy() = default;
Expand Down
Loading
Loading