[call creds] implement GcpServiceAccountIdentityCredentials (#37544)

As per gRFC A83 (https://github.com/grpc/proposal/pull/438).

For now, I am not exposing this new call creds type via the C-core API or in any C++ or wrapped language public APIs, so there's no way to use it externally.  We can easily add that in the future if someone asks, but for now the intent is to use it only internally via the xDS GCP authentication filter, which I'll implement in a subsequent PR.

As part of this, I changed the test framework in credentials_test to check the status code in addition to the message on failure.  This exposed several places where existing credential types are returnign the wrong status code (unsurprisingly, because of all of the tech debt surrounding grpc_error).  I have not fixed this behavior, but I have added TODOs in the test showing which ones I think need to be fixed.

Closes #37544

COPYBARA_INTEGRATE_REVIEW=https://github.com/grpc/grpc/pull/37544 from markdroth:gcp_service_account_identity_call_creds 97e0efc48d
PiperOrigin-RevId: 666869692
pull/37571/head
Mark D. Roth 6 months ago committed by Copybara-Service
parent e9dd477563
commit 1582c3df46
  1. 1
      CMakeLists.txt
  2. 2
      build_autogenerated.yaml
  3. 46
      src/core/BUILD
  4. 196
      src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.cc
  5. 87
      src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.h
  6. 1
      test/core/security/BUILD
  7. 241
      test/core/security/credentials_test.cc

1
CMakeLists.txt generated

@ -30605,6 +30605,7 @@ add_executable(test_core_security_credentials_test
${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.cc
${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.pb.h
${_gRPC_PROTO_GENS_DIR}/test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.grpc.pb.h
src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.cc
test/core/event_engine/event_engine_test_utils.cc
test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
test/core/security/credentials_test.cc

@ -19651,6 +19651,7 @@ targets:
build: test
language: c++
headers:
- src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.h
- test/core/event_engine/event_engine_test_utils.h
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.h
- test/core/test_util/cmdline.h
@ -19665,6 +19666,7 @@ targets:
- test/core/test_util/tracer_util.h
src:
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.proto
- src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.cc
- test/core/event_engine/event_engine_test_utils.cc
- test/core/event_engine/fuzzing_event_engine/fuzzing_event_engine.cc
- test/core/security/credentials_test.cc

@ -4354,6 +4354,52 @@ grpc_cc_library(
],
)
grpc_cc_library(
name = "gcp_service_account_identity_credentials",
srcs = [
"lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.cc",
],
hdrs = [
"lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.h",
],
external_deps = [
"absl/functional:any_invocable",
"absl/status",
"absl/status:statusor",
"absl/strings",
],
language = "c++",
deps = [
"activity",
"arena_promise",
"closure",
"error",
"json",
"json_args",
"json_object_loader",
"json_reader",
"metadata",
"pollset_set",
"ref_counted",
"slice",
"status_conversion",
"status_helper",
"time",
"token_fetcher_credentials",
"unique_type_name",
"//:gpr",
"//:grpc_base",
"//:grpc_core_credentials_header",
"//:grpc_security_base",
"//:httpcli",
"//:iomgr",
"//:orphanable",
"//:promise",
"//:ref_counted_ptr",
"//:uri_parser",
],
)
grpc_cc_library(
name = "grpc_oauth2_credentials",
srcs = [

@ -0,0 +1,196 @@
//
// Copyright 2024 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.h"
#include "absl/functional/any_invocable.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include <grpc/support/time.h>
#include "src/core/lib/gprpp/ref_counted_ptr.h"
#include "src/core/lib/gprpp/status_helper.h"
#include "src/core/lib/iomgr/error.h"
#include "src/core/lib/transport/metadata.h"
#include "src/core/lib/transport/status_conversion.h"
#include "src/core/lib/uri/uri_parser.h"
#include "src/core/util/json/json.h"
#include "src/core/util/json/json_args.h"
#include "src/core/util/json/json_object_loader.h"
#include "src/core/util/json/json_reader.h"
namespace grpc_core {
//
// JwtTokenFetcherCallCredentials
//
// State held for a pending HTTP request.
class JwtTokenFetcherCallCredentials::HttpFetchRequest final
: public TokenFetcherCredentials::FetchRequest {
public:
HttpFetchRequest(
JwtTokenFetcherCallCredentials* creds, Timestamp deadline,
absl::AnyInvocable<
void(absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)>
on_done)
: on_done_(std::move(on_done)) {
GRPC_CLOSURE_INIT(&on_http_response_, OnHttpResponse, this, nullptr);
Ref().release(); // Ref held by HTTP request callback.
http_request_ = creds->StartHttpRequest(creds->pollent(), deadline,
&response_, &on_http_response_);
}
~HttpFetchRequest() override { grpc_http_response_destroy(&response_); }
void Orphan() override {
http_request_.reset();
Unref();
}
private:
static void OnHttpResponse(void* arg, grpc_error_handle error) {
RefCountedPtr<HttpFetchRequest> self(static_cast<HttpFetchRequest*>(arg));
if (!error.ok()) {
// TODO(roth): It shouldn't be necessary to explicitly set the
// status to UNAVAILABLE here. Once the HTTP client code is
// migrated to stop using legacy grpc_error APIs to create
// statuses, we should be able to just propagate the status as-is.
self->on_done_(absl::UnavailableError(StatusToString(error)));
return;
}
if (self->response_.status != 200) {
grpc_status_code status_code =
grpc_http2_status_to_grpc_status(self->response_.status);
if (status_code != GRPC_STATUS_UNAVAILABLE) {
status_code = GRPC_STATUS_UNAUTHENTICATED;
}
self->on_done_(absl::Status(static_cast<absl::StatusCode>(status_code),
absl::StrCat("JWT fetch failed with status ",
self->response_.status)));
return;
}
absl::string_view body(self->response_.body, self->response_.body_length);
// Parse JWT token based on https://datatracker.ietf.org/doc/html/rfc7519.
// We don't do full verification here, just enough to extract the
// expiration time.
// First, split the 3 '.'-delimited parts.
std::vector<absl::string_view> parts = absl::StrSplit(body, '.');
if (parts.size() != 3) {
self->on_done_(absl::UnauthenticatedError("error parsing JWT token"));
return;
}
// Base64-decode the payload.
std::string payload;
if (!absl::WebSafeBase64Unescape(parts[1], &payload)) {
self->on_done_(absl::UnauthenticatedError("error parsing JWT token"));
return;
}
// Parse as JSON.
auto json = JsonParse(payload);
if (!json.ok()) {
self->on_done_(absl::UnauthenticatedError("error parsing JWT token"));
return;
}
// Extract "exp" field.
struct ParsedPayload {
uint64_t exp = 0;
static const JsonLoaderInterface* JsonLoader(const JsonArgs&) {
static const auto kJsonLoader = JsonObjectLoader<ParsedPayload>()
.Field("exp", &ParsedPayload::exp)
.Finish();
return kJsonLoader;
}
};
auto parsed_payload = LoadFromJson<ParsedPayload>(*json, JsonArgs(), "");
if (!parsed_payload.ok()) {
self->on_done_(absl::UnauthenticatedError("error parsing JWT token"));
return;
}
gpr_timespec ts = gpr_time_0(GPR_CLOCK_REALTIME);
ts.tv_sec = parsed_payload->exp;
Timestamp expiration_time = Timestamp::FromTimespecRoundDown(ts);
// Return token object.
self->on_done_(MakeRefCounted<Token>(
Slice::FromCopiedString(absl::StrCat("Bearer ", body)),
expiration_time));
}
OrphanablePtr<HttpRequest> http_request_;
grpc_closure on_http_response_;
grpc_http_response response_;
absl::AnyInvocable<void(
absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)>
on_done_;
};
OrphanablePtr<TokenFetcherCredentials::FetchRequest>
JwtTokenFetcherCallCredentials::FetchToken(
Timestamp deadline,
absl::AnyInvocable<
void(absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)>
on_done) {
return MakeOrphanable<HttpFetchRequest>(this, deadline, std::move(on_done));
}
//
// GcpServiceAccountIdentityCallCredentials
//
std::string GcpServiceAccountIdentityCallCredentials::debug_string() {
return absl::StrCat("GcpServiceAccountIdentityCallCredentials(", audience_,
")");
}
UniqueTypeName GcpServiceAccountIdentityCallCredentials::type() const {
static UniqueTypeName::Factory kFactory("GcpServiceAccountIdentity");
return kFactory.Create();
}
OrphanablePtr<HttpRequest>
GcpServiceAccountIdentityCallCredentials::StartHttpRequest(
grpc_polling_entity* pollent, Timestamp deadline,
grpc_http_response* response, grpc_closure* on_complete) {
grpc_http_header header = {const_cast<char*>("Metadata-Flavor"),
const_cast<char*>("Google")};
grpc_http_request request;
memset(&request, 0, sizeof(grpc_http_request));
request.hdr_count = 1;
request.hdrs = &header;
// TODO(ctiller): Carry the memory quota in ctx and share it with the host
// channel. This would allow us to cancel an authentication query when under
// extreme memory pressure.
auto uri = URI::Create(
"http", "metadata.google.internal.",
"/computeMetadata/v1/instance/service-accounts/default/identity",
{{"audience", audience_}}, /*fragment=*/"");
CHECK_OK(uri); // params are hardcoded
auto http_request =
HttpRequest::Get(std::move(*uri), /*args=*/nullptr, pollent, &request,
deadline, on_complete, response,
RefCountedPtr<grpc_channel_credentials>(
grpc_insecure_credentials_create()));
http_request->Start();
return http_request;
}
} // namespace grpc_core

@ -0,0 +1,87 @@
//
// Copyright 2024 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#ifndef GRPC_SRC_CORE_LIB_SECURITY_CREDENTIALS_GCP_SERVICE_ACCOUNT_IDENTITY_GCP_SERVICE_ACCOUNT_IDENTITY_CREDENTIALS_H
#define GRPC_SRC_CORE_LIB_SECURITY_CREDENTIALS_GCP_SERVICE_ACCOUNT_IDENTITY_GCP_SERVICE_ACCOUNT_IDENTITY_CREDENTIALS_H
#include <string>
#include <utility>
#include <grpc/credentials.h>
#include <grpc/grpc_security.h>
#include "src/core/lib/gprpp/orphanable.h"
#include "src/core/lib/gprpp/ref_counted_ptr.h"
#include "src/core/lib/gprpp/time.h"
#include "src/core/lib/gprpp/unique_type_name.h"
#include "src/core/lib/iomgr/closure.h"
#include "src/core/lib/iomgr/polling_entity.h"
#include "src/core/lib/security/credentials/credentials.h"
#include "src/core/lib/security/credentials/token_fetcher/token_fetcher_credentials.h"
#include "src/core/lib/slice/slice.h"
#include "src/core/lib/transport/metadata.h"
#include "src/core/util/http_client/httpcli.h"
#include "src/core/util/http_client/parser.h"
namespace grpc_core {
// A base class for JWT token fetching credentials.
// Subclasses must implement StartHttpRequest().
class JwtTokenFetcherCallCredentials : public TokenFetcherCredentials {
public:
OrphanablePtr<FetchRequest> FetchToken(
Timestamp deadline,
absl::AnyInvocable<
void(absl::StatusOr<RefCountedPtr<TokenFetcherCredentials::Token>>)>
on_done) final;
private:
class HttpFetchRequest;
virtual OrphanablePtr<HttpRequest> StartHttpRequest(
grpc_polling_entity* pollent, Timestamp deadline,
grpc_http_response* response, grpc_closure* on_complete) = 0;
};
// GCP service account identity call credentials.
class GcpServiceAccountIdentityCallCredentials
: public JwtTokenFetcherCallCredentials {
public:
explicit GcpServiceAccountIdentityCallCredentials(absl::string_view audience)
: audience_(audience) {}
std::string debug_string() override;
UniqueTypeName type() const override;
absl::string_view audience() const { return audience_; }
private:
OrphanablePtr<HttpRequest> StartHttpRequest(
grpc_polling_entity* pollent, Timestamp deadline,
grpc_http_response* response, grpc_closure* on_complete) override;
int cmp_impl(const grpc_call_credentials* other) const override {
// TODO(yashykt): Check if we can do something better here
return QsortCompare(static_cast<const grpc_call_credentials*>(this), other);
}
std::string audience_;
};
} // namespace grpc_core
#endif // GRPC_SRC_CORE_LIB_SECURITY_CREDENTIALS_GCP_SERVICE_ACCOUNT_IDENTITY_GCP_SERVICE_ACCOUNT_IDENTITY_CREDENTIALS_H

@ -111,6 +111,7 @@ grpc_cc_test(
"//:gpr",
"//:grpc",
"//src/core:channel_args",
"//src/core:gcp_service_account_identity_credentials",
"//test/core/event_engine:event_engine_test_utils",
"//test/core/event_engine/fuzzing_event_engine",
"//test/core/test_util:grpc_test_util",

@ -60,6 +60,7 @@
#include "src/core/lib/security/credentials/external/file_external_account_credentials.h"
#include "src/core/lib/security/credentials/external/url_external_account_credentials.h"
#include "src/core/lib/security/credentials/fake/fake_credentials.h"
#include "src/core/lib/security/credentials/gcp_service_account_identity/gcp_service_account_identity_credentials.h"
#include "src/core/lib/security/credentials/google_default/google_default_credentials.h"
#include "src/core/lib/security/credentials/iam/iam_credentials.h"
#include "src/core/lib/security/credentials/jwt/jwt_credentials.h"
@ -519,12 +520,13 @@ class RequestMetadataState : public RefCounted<RequestMetadataState> {
if (expected_error_.ok()) {
ASSERT_TRUE(error.ok()) << error;
} else {
std::string expected_error;
grpc_error_get_str(expected_error_, StatusStrProperty::kDescription,
&expected_error);
std::string actual_error;
grpc_error_get_str(error, StatusStrProperty::kDescription, &actual_error);
EXPECT_EQ(expected_error, actual_error);
grpc_status_code actual_code;
std::string actual_message;
grpc_error_get_status(error, Timestamp::InfFuture(), &actual_code,
&actual_message, nullptr, nullptr);
EXPECT_EQ(absl::Status(static_cast<absl::StatusCode>(actual_code),
actual_message),
expected_error_);
}
md_.Remove(HttpAuthorityMetadata());
md_.Remove(HttpPathMetadata());
@ -810,7 +812,8 @@ TEST_F(CredentialsTest, TestComputeEngineCredsFailure) {
"GoogleComputeEngineTokenFetcherCredentials{"
"OAuth2TokenFetcherCredentials}";
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE("error parsing oauth2 token"), {});
// TODO(roth): This should return UNAUTHENTICATED.
absl::UnavailableError("error parsing oauth2 token"), {});
grpc_call_credentials* creds =
grpc_google_compute_engine_credentials_create(nullptr);
HttpRequest::SetOverride(compute_engine_httpcli_get_failure_override,
@ -902,7 +905,8 @@ TEST_F(CredentialsTest, TestRefreshTokenCredsFailure) {
"GoogleRefreshToken{ClientID:32555999999.apps.googleusercontent.com,"
"OAuth2TokenFetcherCredentials}";
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE("error parsing oauth2 token"), {});
// TODO(roth): This should return UNAUTHENTICATED.
absl::UnavailableError("error parsing oauth2 token"), {});
grpc_call_credentials* creds = grpc_google_refresh_token_credentials_create(
test_refresh_token_str, nullptr);
HttpRequest::SetOverride(httpcli_get_should_not_be_called,
@ -1162,7 +1166,8 @@ TEST_F(CredentialsTest, TestStsCredsTokenFileNotFound) {
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAVAILABLE.
absl::InternalError(
"Failed to load file: /some/completely/random/path due to "
"error(fdopen): No such file or directory"),
{});
@ -1233,8 +1238,9 @@ TEST_F(CredentialsTest, TestStsCredsLoadTokenFailure) {
"token-exchange,Authority:foo.com:5555,OAuth2TokenFetcherCredentials}";
ExecCtx exec_ctx;
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE("Failed to load file: invalid_path due to "
"error(fdopen): No such file or directory"),
// TODO(roth): This should return UNAVAILABLE.
absl::InternalError("Failed to load file: invalid_path due to "
"error(fdopen): No such file or directory"),
{});
char* test_signed_jwt_path = write_tmp_jwt_file(test_signed_jwt);
grpc_sts_credentials_options options = {
@ -1268,7 +1274,8 @@ TEST_F(CredentialsTest, TestStsCredsHttpFailure) {
"token-exchange,Authority:foo.com:5555,OAuth2TokenFetcherCredentials}";
ExecCtx exec_ctx;
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE("error parsing oauth2 token"), {});
// TODO(roth): This should return UNAUTHENTICATED.
absl::UnavailableError("error parsing oauth2 token"), {});
char* test_signed_jwt_path = write_tmp_jwt_file(test_signed_jwt);
grpc_sts_credentials_options valid_options = {
test_sts_endpoint_url, // sts_endpoint_url
@ -1465,7 +1472,7 @@ TEST_F(CredentialsTest, TestJwtCredsSigningFailure) {
char* json_key_string = test_json_key_str();
ExecCtx exec_ctx;
auto state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE("Could not generate JWT."), {});
absl::UnauthenticatedError("Could not generate JWT."), {});
grpc_call_credentials* creds =
grpc_service_account_jwt_access_credentials_create(
json_key_string, grpc_max_auth_token_lifetime(), nullptr);
@ -1924,7 +1931,8 @@ TEST_F(CredentialsTest, TestMetadataPluginFailure) {
grpc_metadata_credentials_plugin plugin;
ExecCtx exec_ctx;
auto md_state = RequestMetadataState::NewInstance(
GRPC_ERROR_CREATE(
// TODO(roth): Is this the right status to use here?
absl::UnavailableError(
absl::StrCat("Getting metadata from plugin failed with error: ",
plugin_error_details)),
{});
@ -3058,7 +3066,8 @@ TEST_F(ExternalAccountCredentialsTest, FailureInvalidTokenUrl) {
HttpRequest::SetOverride(httpcli_get_should_not_be_called,
httpcli_post_should_not_be_called,
httpcli_put_should_not_be_called);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: Invalid token url: "
"invalid_token_url. Error: INVALID_ARGUMENT: Could not parse "
"'scheme' from uri 'invalid_token_url'. Scheme not found.");
@ -3096,7 +3105,8 @@ TEST_F(ExternalAccountCredentialsTest,
HttpRequest::SetOverride(httpcli_get_should_not_be_called,
external_account_creds_httpcli_post_success,
httpcli_put_should_not_be_called);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: Invalid service account impersonation url: "
"invalid_service_account_impersonation_url. Error: INVALID_ARGUMENT: "
"Could not parse 'scheme' from uri "
@ -3136,7 +3146,8 @@ TEST_F(ExternalAccountCredentialsTest,
httpcli_get_should_not_be_called,
external_account_creds_httpcli_post_failure_token_exchange_response_missing_access_token,
httpcli_put_should_not_be_called);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: Missing or invalid access_token in "
"{\"not_access_token\":\"not_access_token\",\"expires_in\":3599, "
"\"token_type\":\"Bearer\"}.");
@ -3424,7 +3435,8 @@ TEST_F(ExternalAccountCredentialsTest,
HttpRequest::SetOverride(httpcli_get_should_not_be_called,
httpcli_post_should_not_be_called,
httpcli_put_should_not_be_called);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAVAILABLE.
grpc_error_handle expected_error = absl::InternalError(
"error fetching oauth2 token: Failed to load file: "
"non_exisiting_file due to error(fdopen): No such file or directory");
auto state = RequestMetadataState::NewInstance(expected_error, {});
@ -3474,7 +3486,8 @@ TEST_F(ExternalAccountCredentialsTest,
HttpRequest::SetOverride(httpcli_get_should_not_be_called,
httpcli_post_should_not_be_called,
httpcli_put_should_not_be_called);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: The content of the file is not a "
"valid json object.");
auto state = RequestMetadataState::NewInstance(expected_error, {});
@ -4140,7 +4153,8 @@ TEST_F(ExternalAccountCredentialsTest,
ASSERT_TRUE(creds.ok()) << creds.status();
ASSERT_NE(*creds, nullptr);
EXPECT_EQ((*creds)->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: Creating aws request signer failed.");
auto state = RequestMetadataState::NewInstance(expected_error, {});
HttpRequest::SetOverride(aws_external_account_creds_httpcli_get_success,
@ -4181,7 +4195,8 @@ TEST_F(ExternalAccountCredentialsTest,
ASSERT_TRUE(creds.ok()) << creds.status();
ASSERT_NE(*creds, nullptr);
EXPECT_EQ((*creds)->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
grpc_error_handle expected_error = GRPC_ERROR_CREATE(
// TODO(roth): This should return UNAUTHENTICATED.
grpc_error_handle expected_error = absl::UnknownError(
"error fetching oauth2 token: "
"Missing role name when retrieving signing keys.");
auto state = RequestMetadataState::NewInstance(expected_error, {});
@ -4473,6 +4488,190 @@ TEST_F(CredentialsTest, TestXdsCredentialsCompareFailure) {
grpc_channel_credentials_release(xds_creds_2);
}
class GcpServiceAccountIdentityCredentialsTest : public ::testing::Test {
protected:
void SetUp() override {
g_http_status = 200;
g_audience = "";
g_token = nullptr;
g_on_http_request_error = nullptr;
HttpRequest::SetOverride(HttpGetOverride, httpcli_post_should_not_be_called,
httpcli_put_should_not_be_called);
}
void TearDown() override {
HttpRequest::SetOverride(nullptr, nullptr, nullptr);
}
static void ValidateHttpRequest(const grpc_http_request* request,
const URI& uri) {
EXPECT_EQ(uri.authority(), "metadata.google.internal.");
EXPECT_EQ(uri.path(),
"/computeMetadata/v1/instance/service-accounts/default/identity");
EXPECT_THAT(
uri.query_parameter_map(),
::testing::ElementsAre(::testing::Pair("audience", g_audience)));
ASSERT_EQ(request->hdr_count, 1);
EXPECT_EQ(absl::string_view(request->hdrs[0].key), "Metadata-Flavor");
EXPECT_EQ(absl::string_view(request->hdrs[0].value), "Google");
}
static int HttpGetOverride(const grpc_http_request* request, const URI& uri,
Timestamp /*deadline*/, grpc_closure* on_done,
grpc_http_response* response) {
// Validate request.
ValidateHttpRequest(request, uri);
// Generate response.
*response = http_response(g_http_status, g_token == nullptr ? "" : g_token);
ExecCtx::Run(DEBUG_LOCATION, on_done,
g_on_http_request_error == nullptr ? absl::OkStatus()
: *g_on_http_request_error);
return 1;
}
// Constructs a synthetic JWT token that's just valid enough for the
// call creds to extract the expiration date.
static std::string MakeToken(Timestamp expiration) {
gpr_timespec ts = expiration.as_timespec(GPR_CLOCK_REALTIME);
std::string json = absl::StrCat("{\"exp\":", ts.tv_sec, "}");
return absl::StrCat("foo.", absl::WebSafeBase64Escape(json), ".bar");
}
static int g_http_status;
static absl::string_view g_audience;
static const char* g_token;
static absl::Status* g_on_http_request_error;
};
int GcpServiceAccountIdentityCredentialsTest::g_http_status;
absl::string_view GcpServiceAccountIdentityCredentialsTest::g_audience;
const char* GcpServiceAccountIdentityCredentialsTest::g_token;
absl::Status* GcpServiceAccountIdentityCredentialsTest::g_on_http_request_error;
TEST_F(GcpServiceAccountIdentityCredentialsTest, Basic) {
g_audience = "CV-6";
auto token = MakeToken(Timestamp::Now() + Duration::Hours(1));
g_token = token.c_str();
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(absl::OkStatus(), g_token);
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
// HTTP status 429 is mapped to UNAVAILABLE as per
// https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md.
TEST_F(GcpServiceAccountIdentityCredentialsTest, FailsWithHttpStatus429) {
g_audience = "CV-5_Midway";
g_http_status = 429;
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnavailableError("JWT fetch failed with status 429"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
// HTTP status 400 is mapped to INTERNAL as per
// https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md,
// so it should be rewritten as UNAUTHENTICATED.
TEST_F(GcpServiceAccountIdentityCredentialsTest, FailsWithHttpStatus400) {
g_audience = "CV-8_SantaCruzIslands";
g_http_status = 400;
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnauthenticatedError("JWT fetch failed with status 400"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
TEST_F(GcpServiceAccountIdentityCredentialsTest, FailsWithHttpIOError) {
g_audience = "CV-2_CoralSea";
absl::Status status = absl::InternalError("uh oh");
g_on_http_request_error = &status;
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnavailableError("INTERNAL:uh oh"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
TEST_F(GcpServiceAccountIdentityCredentialsTest, TokenHasWrongNumberOfDots) {
g_audience = "CV-7_Guadalcanal";
std::string bad_token = "foo.bar";
g_token = bad_token.c_str();
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnauthenticatedError("error parsing JWT token"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
TEST_F(GcpServiceAccountIdentityCredentialsTest, TokenPayloadNotBase64) {
g_audience = "CVE-56_Makin";
std::string bad_token = "foo.&.bar";
g_token = bad_token.c_str();
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnauthenticatedError("error parsing JWT token"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
TEST_F(GcpServiceAccountIdentityCredentialsTest, TokenPayloadNotJson) {
g_audience = "CVE-73_Samar";
std::string bad_token =
absl::StrCat("foo.", absl::WebSafeBase64Escape("xxx"), ".bar");
g_token = bad_token.c_str();
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnauthenticatedError("error parsing JWT token"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
TEST_F(GcpServiceAccountIdentityCredentialsTest, TokenInvalidExpiration) {
g_audience = "CVL-23_Leyte";
std::string bad_token = absl::StrCat(
"foo.", absl::WebSafeBase64Escape("{\"exp\":\"foo\"}"), ".bar");
g_token = bad_token.c_str();
ExecCtx exec_ctx;
auto creds =
MakeRefCounted<GcpServiceAccountIdentityCallCredentials>(g_audience);
CHECK_EQ(creds->min_security_level(), GRPC_PRIVACY_AND_INTEGRITY);
auto state = RequestMetadataState::NewInstance(
absl::UnauthenticatedError("error parsing JWT token"), "");
state->RunRequestMetadataTest(creds.get(), kTestUrlScheme, kTestAuthority,
kTestPath);
ExecCtx::Get()->Flush();
}
} // namespace
} // namespace grpc_core

Loading…
Cancel
Save