mirror of https://github.com/grpc/grpc.git
[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
parent
e9dd477563
commit
1582c3df46
7 changed files with 553 additions and 21 deletions
@ -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
|
Loading…
Reference in new issue