mirror of
https://github.com/rocky-linux/peridot.git
synced 2024-11-23 21:51:27 +00:00
385 lines
13 KiB
Go
385 lines
13 KiB
Go
// Copyright 2023 Google LLC
|
|
//
|
|
// 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.
|
|
|
|
package grpctransport
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"cloud.google.com/go/auth"
|
|
"cloud.google.com/go/auth/credentials"
|
|
"cloud.google.com/go/auth/internal"
|
|
"cloud.google.com/go/auth/internal/transport"
|
|
"go.opencensus.io/plugin/ocgrpc"
|
|
"google.golang.org/grpc"
|
|
grpccreds "google.golang.org/grpc/credentials"
|
|
grpcinsecure "google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const (
|
|
// Check env to disable DirectPath traffic.
|
|
disableDirectPathEnvVar = "GOOGLE_CLOUD_DISABLE_DIRECT_PATH"
|
|
|
|
// Check env to decide if using google-c2p resolver for DirectPath traffic.
|
|
enableDirectPathXdsEnvVar = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS"
|
|
|
|
quotaProjectHeaderKey = "X-Goog-User-Project"
|
|
)
|
|
|
|
var (
|
|
// Set at init time by dial_socketopt.go. If nil, socketopt is not supported.
|
|
timeoutDialerOption grpc.DialOption
|
|
)
|
|
|
|
// ClientCertProvider is a function that returns a TLS client certificate to be
|
|
// used when opening TLS connections. It follows the same semantics as
|
|
// [crypto/tls.Config.GetClientCertificate].
|
|
type ClientCertProvider = func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
|
|
|
// Options used to configure a [GRPCClientConnPool] from [Dial].
|
|
type Options struct {
|
|
// DisableTelemetry disables default telemetry (OpenTelemetry). An example
|
|
// reason to do so would be to bind custom telemetry that overrides the
|
|
// defaults.
|
|
DisableTelemetry bool
|
|
// DisableAuthentication specifies that no authentication should be used. It
|
|
// is suitable only for testing and for accessing public resources, like
|
|
// public Google Cloud Storage buckets.
|
|
DisableAuthentication bool
|
|
// Endpoint overrides the default endpoint to be used for a service.
|
|
Endpoint string
|
|
// Metadata is extra gRPC metadata that will be appended to every outgoing
|
|
// request.
|
|
Metadata map[string]string
|
|
// GRPCDialOpts are dial options that will be passed to `grpc.Dial` when
|
|
// establishing a`grpc.Conn``
|
|
GRPCDialOpts []grpc.DialOption
|
|
// PoolSize is specifies how many connections to balance between when making
|
|
// requests. If unset or less than 1, the value defaults to 1.
|
|
PoolSize int
|
|
// Credentials used to add Authorization metadata to all requests. If set
|
|
// DetectOpts are ignored.
|
|
Credentials *auth.Credentials
|
|
// ClientCertProvider is a function that returns a TLS client certificate to
|
|
// be used when opening TLS connections. It follows the same semantics as
|
|
// crypto/tls.Config.GetClientCertificate.
|
|
ClientCertProvider ClientCertProvider
|
|
// DetectOpts configures settings for detect Application Default
|
|
// Credentials.
|
|
DetectOpts *credentials.DetectOptions
|
|
// UniverseDomain is the default service domain for a given Cloud universe.
|
|
// The default value is "googleapis.com". This is the universe domain
|
|
// configured for the client, which will be compared to the universe domain
|
|
// that is separately configured for the credentials.
|
|
UniverseDomain string
|
|
// APIKey specifies an API key to be used as the basis for authentication.
|
|
// If set DetectOpts are ignored.
|
|
APIKey string
|
|
|
|
// InternalOptions are NOT meant to be set directly by consumers of this
|
|
// package, they should only be set by generated client code.
|
|
InternalOptions *InternalOptions
|
|
}
|
|
|
|
// client returns the client a user set for the detect options or nil if one was
|
|
// not set.
|
|
func (o *Options) client() *http.Client {
|
|
if o.DetectOpts != nil && o.DetectOpts.Client != nil {
|
|
return o.DetectOpts.Client
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *Options) validate() error {
|
|
if o == nil {
|
|
return errors.New("grpctransport: opts required to be non-nil")
|
|
}
|
|
if o.InternalOptions != nil && o.InternalOptions.SkipValidation {
|
|
return nil
|
|
}
|
|
hasCreds := o.APIKey != "" ||
|
|
o.Credentials != nil ||
|
|
(o.DetectOpts != nil && len(o.DetectOpts.CredentialsJSON) > 0) ||
|
|
(o.DetectOpts != nil && o.DetectOpts.CredentialsFile != "")
|
|
if o.DisableAuthentication && hasCreds {
|
|
return errors.New("grpctransport: DisableAuthentication is incompatible with options that set or detect credentials")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *Options) resolveDetectOptions() *credentials.DetectOptions {
|
|
io := o.InternalOptions
|
|
// soft-clone these so we are not updating a ref the user holds and may reuse
|
|
do := transport.CloneDetectOptions(o.DetectOpts)
|
|
|
|
// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
|
|
if (io != nil && io.EnableJWTWithScope) || do.Audience != "" {
|
|
do.UseSelfSignedJWT = true
|
|
}
|
|
// Only default scopes if user did not also set an audience.
|
|
if len(do.Scopes) == 0 && do.Audience == "" && io != nil && len(io.DefaultScopes) > 0 {
|
|
do.Scopes = make([]string, len(io.DefaultScopes))
|
|
copy(do.Scopes, io.DefaultScopes)
|
|
}
|
|
if len(do.Scopes) == 0 && do.Audience == "" && io != nil {
|
|
do.Audience = o.InternalOptions.DefaultAudience
|
|
}
|
|
if o.ClientCertProvider != nil {
|
|
tlsConfig := &tls.Config{
|
|
GetClientCertificate: o.ClientCertProvider,
|
|
}
|
|
do.Client = transport.DefaultHTTPClientWithTLS(tlsConfig)
|
|
do.TokenURL = credentials.GoogleMTLSTokenURL
|
|
}
|
|
return do
|
|
}
|
|
|
|
// InternalOptions are only meant to be set by generated client code. These are
|
|
// not meant to be set directly by consumers of this package. Configuration in
|
|
// this type is considered EXPERIMENTAL and may be removed at any time in the
|
|
// future without warning.
|
|
type InternalOptions struct {
|
|
// EnableNonDefaultSAForDirectPath overrides the default requirement for
|
|
// using the default service account for DirectPath.
|
|
EnableNonDefaultSAForDirectPath bool
|
|
// EnableDirectPath overrides the default attempt to use DirectPath.
|
|
EnableDirectPath bool
|
|
// EnableDirectPathXds overrides the default DirectPath type. It is only
|
|
// valid when DirectPath is enabled.
|
|
EnableDirectPathXds bool
|
|
// EnableJWTWithScope specifies if scope can be used with self-signed JWT.
|
|
EnableJWTWithScope bool
|
|
// DefaultAudience specifies a default audience to be used as the audience
|
|
// field ("aud") for the JWT token authentication.
|
|
DefaultAudience string
|
|
// DefaultEndpointTemplate combined with UniverseDomain specifies
|
|
// the default endpoint.
|
|
DefaultEndpointTemplate string
|
|
// DefaultMTLSEndpoint specifies the default mTLS endpoint.
|
|
DefaultMTLSEndpoint string
|
|
// DefaultScopes specifies the default OAuth2 scopes to be used for a
|
|
// service.
|
|
DefaultScopes []string
|
|
// SkipValidation bypasses validation on Options. It should only be used
|
|
// internally for clients that needs more control over their transport.
|
|
SkipValidation bool
|
|
}
|
|
|
|
// Dial returns a GRPCClientConnPool that can be used to communicate with a
|
|
// Google cloud service, configured with the provided [Options]. It
|
|
// automatically appends Authorization metadata to all outgoing requests.
|
|
func Dial(ctx context.Context, secure bool, opts *Options) (GRPCClientConnPool, error) {
|
|
if err := opts.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if opts.PoolSize <= 1 {
|
|
conn, err := dial(ctx, secure, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &singleConnPool{conn}, nil
|
|
}
|
|
pool := &roundRobinConnPool{}
|
|
for i := 0; i < opts.PoolSize; i++ {
|
|
conn, err := dial(ctx, secure, opts)
|
|
if err != nil {
|
|
// ignore close error, if any
|
|
defer pool.Close()
|
|
return nil, err
|
|
}
|
|
pool.conns = append(pool.conns, conn)
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
// return a GRPCClientConnPool if pool == 1 or else a pool of of them if >1
|
|
func dial(ctx context.Context, secure bool, opts *Options) (*grpc.ClientConn, error) {
|
|
tOpts := &transport.Options{
|
|
Endpoint: opts.Endpoint,
|
|
ClientCertProvider: opts.ClientCertProvider,
|
|
Client: opts.client(),
|
|
UniverseDomain: opts.UniverseDomain,
|
|
}
|
|
if io := opts.InternalOptions; io != nil {
|
|
tOpts.DefaultEndpointTemplate = io.DefaultEndpointTemplate
|
|
tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint
|
|
tOpts.EnableDirectPath = io.EnableDirectPath
|
|
tOpts.EnableDirectPathXds = io.EnableDirectPathXds
|
|
}
|
|
transportCreds, endpoint, err := transport.GetGRPCTransportCredsAndEndpoint(tOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !secure {
|
|
transportCreds = grpcinsecure.NewCredentials()
|
|
}
|
|
|
|
// Initialize gRPC dial options with transport-level security options.
|
|
grpcOpts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(transportCreds),
|
|
}
|
|
|
|
// Ensure the token exchange HTTP transport uses the same ClientCertProvider as the GRPC API transport.
|
|
opts.ClientCertProvider, err = transport.GetClientCertificateProvider(tOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if opts.APIKey != "" {
|
|
grpcOpts = append(grpcOpts,
|
|
grpc.WithPerRPCCredentials(&grpcKeyProvider{
|
|
apiKey: opts.APIKey,
|
|
metadata: opts.Metadata,
|
|
secure: secure,
|
|
}),
|
|
)
|
|
} else if !opts.DisableAuthentication {
|
|
metadata := opts.Metadata
|
|
|
|
var creds *auth.Credentials
|
|
if opts.Credentials != nil {
|
|
creds = opts.Credentials
|
|
} else {
|
|
var err error
|
|
creds, err = credentials.DetectDefault(opts.resolveDetectOptions())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
qp, err := creds.QuotaProjectID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if qp != "" {
|
|
if metadata == nil {
|
|
metadata = make(map[string]string, 1)
|
|
}
|
|
metadata[quotaProjectHeaderKey] = qp
|
|
}
|
|
grpcOpts = append(grpcOpts,
|
|
grpc.WithPerRPCCredentials(&grpcCredentialsProvider{
|
|
creds: creds,
|
|
metadata: metadata,
|
|
clientUniverseDomain: opts.UniverseDomain,
|
|
}),
|
|
)
|
|
|
|
// Attempt Direct Path
|
|
grpcOpts, endpoint = configureDirectPath(grpcOpts, opts, endpoint, creds)
|
|
}
|
|
|
|
// Add tracing, but before the other options, so that clients can override the
|
|
// gRPC stats handler.
|
|
// This assumes that gRPC options are processed in order, left to right.
|
|
grpcOpts = addOCStatsHandler(grpcOpts, opts)
|
|
grpcOpts = append(grpcOpts, opts.GRPCDialOpts...)
|
|
|
|
return grpc.DialContext(ctx, endpoint, grpcOpts...)
|
|
}
|
|
|
|
// grpcKeyProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials.
|
|
type grpcKeyProvider struct {
|
|
apiKey string
|
|
metadata map[string]string
|
|
secure bool
|
|
}
|
|
|
|
func (g *grpcKeyProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
|
metadata := make(map[string]string, len(g.metadata)+1)
|
|
metadata["X-goog-api-key"] = g.apiKey
|
|
for k, v := range g.metadata {
|
|
metadata[k] = v
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func (g *grpcKeyProvider) RequireTransportSecurity() bool {
|
|
return g.secure
|
|
}
|
|
|
|
// grpcCredentialsProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials.
|
|
type grpcCredentialsProvider struct {
|
|
creds *auth.Credentials
|
|
|
|
secure bool
|
|
|
|
// Additional metadata attached as headers.
|
|
metadata map[string]string
|
|
clientUniverseDomain string
|
|
}
|
|
|
|
// getClientUniverseDomain returns the default service domain for a given Cloud universe.
|
|
// The default value is "googleapis.com". This is the universe domain
|
|
// configured for the client, which will be compared to the universe domain
|
|
// that is separately configured for the credentials.
|
|
func (c *grpcCredentialsProvider) getClientUniverseDomain() string {
|
|
if c.clientUniverseDomain == "" {
|
|
return internal.DefaultUniverseDomain
|
|
}
|
|
return c.clientUniverseDomain
|
|
}
|
|
|
|
func (c *grpcCredentialsProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
|
credentialsUniverseDomain, err := c.creds.UniverseDomain(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := transport.ValidateUniverseDomain(c.getClientUniverseDomain(), credentialsUniverseDomain); err != nil {
|
|
return nil, err
|
|
}
|
|
token, err := c.creds.Token(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.secure {
|
|
ri, _ := grpccreds.RequestInfoFromContext(ctx)
|
|
if err = grpccreds.CheckSecurityLevel(ri.AuthInfo, grpccreds.PrivacyAndIntegrity); err != nil {
|
|
return nil, fmt.Errorf("unable to transfer credentials PerRPCCredentials: %v", err)
|
|
}
|
|
}
|
|
metadata := make(map[string]string, len(c.metadata)+1)
|
|
setAuthMetadata(token, metadata)
|
|
for k, v := range c.metadata {
|
|
metadata[k] = v
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
// setAuthMetadata uses the provided token to set the Authorization metadata.
|
|
// If the token.Type is empty, the type is assumed to be Bearer.
|
|
func setAuthMetadata(token *auth.Token, m map[string]string) {
|
|
typ := token.Type
|
|
if typ == "" {
|
|
typ = internal.TokenTypeBearer
|
|
}
|
|
m["authorization"] = typ + " " + token.Value
|
|
}
|
|
|
|
func (c *grpcCredentialsProvider) RequireTransportSecurity() bool {
|
|
return c.secure
|
|
}
|
|
|
|
func addOCStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption {
|
|
if opts.DisableTelemetry {
|
|
return dialOpts
|
|
}
|
|
return append(dialOpts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
|
|
}
|