From 69a83470bdcc7ed10c6c36d1abc3b7cfdb8a0ee5 Mon Sep 17 00:00:00 2001 From: Cody Oss <6331106+codyoss@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:09:07 -0500 Subject: [PATCH] feat(auth): add grpctransport package (#8625) This package is the analog to https://pkg.go.dev/google.golang.org/api/transport/grpc. --- auth/go.mod | 5 +- auth/go.sum | 8 + auth/grpctransport/dial_socketopt.go | 62 ++++ auth/grpctransport/dial_socketopt_test.go | 124 ++++++++ auth/grpctransport/directpath.go | 123 ++++++++ auth/grpctransport/grpctransport.go | 279 +++++++++++++++++ auth/grpctransport/grpctransport_test.go | 314 ++++++++++++++++++++ auth/grpctransport/pool.go | 119 ++++++++ auth/grpctransport/pool_test.go | 147 +++++++++ auth/grpctransport/testdata/README.md | 15 + auth/grpctransport/testdata/echo.pb.go | 227 ++++++++++++++ auth/grpctransport/testdata/echo.proto | 31 ++ auth/grpctransport/testdata/echo_grpc.pb.go | 124 ++++++++ auth/impersonate/integration_test.go | 115 +++---- auth/internal/transport/cba.go | 9 +- auth/internal/transport/cba_test.go | 2 +- auth/oauth2adapt/go.sum | 11 + 17 files changed, 1652 insertions(+), 63 deletions(-) create mode 100644 auth/grpctransport/dial_socketopt.go create mode 100644 auth/grpctransport/dial_socketopt_test.go create mode 100644 auth/grpctransport/directpath.go create mode 100644 auth/grpctransport/grpctransport.go create mode 100644 auth/grpctransport/grpctransport_test.go create mode 100644 auth/grpctransport/pool.go create mode 100644 auth/grpctransport/pool_test.go create mode 100644 auth/grpctransport/testdata/README.md create mode 100644 auth/grpctransport/testdata/echo.pb.go create mode 100644 auth/grpctransport/testdata/echo.proto create mode 100644 auth/grpctransport/testdata/echo_grpc.pb.go diff --git a/auth/go.mod b/auth/go.mod index ccf99b4a6555..279721d304a3 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -10,6 +10,7 @@ require ( go.opencensus.io v0.24.0 golang.org/x/net v0.14.0 google.golang.org/grpc v1.57.0 + google.golang.org/protobuf v1.31.0 ) require ( @@ -17,8 +18,10 @@ require ( github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.3 // indirect golang.org/x/crypto v0.12.0 // indirect + golang.org/x/oauth2 v0.7.0 // indirect + golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/auth/go.sum b/auth/go.sum index 4ae253b98310..2fa6c4f2cd13 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -18,6 +18,7 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18h github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -66,14 +67,18 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -81,6 +86,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -92,6 +98,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= diff --git a/auth/grpctransport/dial_socketopt.go b/auth/grpctransport/dial_socketopt.go new file mode 100644 index 000000000000..e6136080572a --- /dev/null +++ b/auth/grpctransport/dial_socketopt.go @@ -0,0 +1,62 @@ +// 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. + +//go:build linux +// +build linux + +package grpctransport + +import ( + "context" + "net" + "syscall" + + "google.golang.org/grpc" +) + +const ( + // defaultTCPUserTimeout is the default TCP_USER_TIMEOUT socket option. By + // default is 20 seconds. + tcpUserTimeoutMilliseconds = 20000 + + // Copied from golang.org/x/sys/unix.TCP_USER_TIMEOUT. + tcpUserTimeoutOp = 0x12 +) + +func init() { + // timeoutDialerOption is a grpc.DialOption that contains dialer with + // socket option TCP_USER_TIMEOUT. This dialer requires go versions 1.11+. + timeoutDialerOption = grpc.WithContextDialer(dialTCPUserTimeout) +} + +func dialTCPUserTimeout(ctx context.Context, addr string) (net.Conn, error) { + control := func(network, address string, c syscall.RawConn) error { + var syscallErr error + controlErr := c.Control(func(fd uintptr) { + syscallErr = syscall.SetsockoptInt( + int(fd), syscall.IPPROTO_TCP, tcpUserTimeoutOp, tcpUserTimeoutMilliseconds) + }) + if syscallErr != nil { + return syscallErr + } + if controlErr != nil { + return controlErr + } + return nil + } + d := &net.Dialer{ + Control: control, + } + return d.DialContext(ctx, "tcp", addr) +} diff --git a/auth/grpctransport/dial_socketopt_test.go b/auth/grpctransport/dial_socketopt_test.go new file mode 100644 index 000000000000..1004df18da3f --- /dev/null +++ b/auth/grpctransport/dial_socketopt_test.go @@ -0,0 +1,124 @@ +// 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. + +//go:build linux +// +build linux + +package grpctransport + +import ( + "context" + "errors" + "fmt" + "net" + "syscall" + "testing" + "time" + + "google.golang.org/grpc" +) + +func TestDialTCPUserTimeout(t *testing.T) { + l, err := net.Listen("tcp", ":3000") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + acceptErrCh := make(chan error, 1) + + go func() { + conn, err := l.Accept() + if err != nil { + acceptErrCh <- err + return + } + defer conn.Close() + + if err := conn.Close(); err != nil { + acceptErrCh <- err + } + }() + + conn, err := dialTCPUserTimeout(context.Background(), ":3000") + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + timeout, err := getTCPUserTimeout(conn) + if err != nil { + t.Fatal(err) + } + if timeout != tcpUserTimeoutMilliseconds { + t.Fatalf("expected %v, got %v", tcpUserTimeoutMilliseconds, timeout) + } + + select { + case err := <-acceptErrCh: + t.Fatalf("Accept failed with: %v", err) + default: + } +} + +func getTCPUserTimeout(conn net.Conn) (int, error) { + tcpConn, ok := conn.(*net.TCPConn) + if !ok { + return 0, fmt.Errorf("conn is not *net.TCPConn. got %T", conn) + } + rawConn, err := tcpConn.SyscallConn() + if err != nil { + return 0, err + } + var timeout int + var syscallErr error + controlErr := rawConn.Control(func(fd uintptr) { + timeout, syscallErr = syscall.GetsockoptInt(int(fd), syscall.IPPROTO_TCP, tcpUserTimeoutOp) + }) + if syscallErr != nil { + return 0, syscallErr + } + if controlErr != nil { + return 0, controlErr + } + return timeout, nil +} + +// Check that tcp timeout dialer overwrites user defined dialer. +func TestDialWithDirectPathEnabled(t *testing.T) { + t.Skip("https://github.com/googleapis/google-api-go-client/issues/790") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + + userDialer := grpc.WithDialer(func(addr string, timeout time.Duration) (net.Conn, error) { + t.Error("did not expect a call to user dialer, got one") + cancel() + return nil, errors.New("not expected") + }) + + pool, err := Dial(ctx, true, &Options{ + TokenProvider: staticTP("hey"), + GRPCDialOpts: []grpc.DialOption{userDialer}, + Endpoint: "example.google.com:443", + InternalOptions: &InternalOptions{ + EnableDirectPath: true, + }, + }) + if err != nil { + t.Errorf("DialGRPC: error %v, want nil", err) + } + defer pool.Close() + + // gRPC doesn't connect before the first call. + grpc.Invoke(ctx, "foo", nil, nil, pool.Connection()) +} diff --git a/auth/grpctransport/directpath.go b/auth/grpctransport/directpath.go new file mode 100644 index 000000000000..652d8feeeb1f --- /dev/null +++ b/auth/grpctransport/directpath.go @@ -0,0 +1,123 @@ +// 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" + "net" + "os" + "strconv" + "strings" + + "cloud.google.com/go/auth" + "cloud.google.com/go/compute/metadata" + "google.golang.org/grpc" + grpcgoogle "google.golang.org/grpc/credentials/google" +) + +func isDirectPathEnabled(endpoint string, opts *Options) bool { + if opts.InternalOptions != nil && !opts.InternalOptions.EnableDirectPath { + return false + } + if !checkDirectPathEndPoint(endpoint) { + return false + } + if b, _ := strconv.ParseBool(os.Getenv(disableDirectPathEnvVar)); b { + return false + } + return true +} + +func checkDirectPathEndPoint(endpoint string) bool { + // Only [dns:///]host[:port] is supported, not other schemes (e.g., "tcp://" or "unix://"). + // Also don't try direct path if the user has chosen an alternate name resolver + // (i.e., via ":///" prefix). + if strings.Contains(endpoint, "://") && !strings.HasPrefix(endpoint, "dns:///") { + return false + } + + if endpoint == "" { + return false + } + + return true +} + +func isTokenProviderDirectPathCompatible(tp auth.TokenProvider, opts *Options) bool { + if tp == nil { + return false + } + tok, err := tp.Token(context.Background()) + if err != nil { + return false + } + if tok == nil { + return false + } + if source, _ := tok.Metadata["auth.google.tokenSource"].(string); source != "compute-metadata" { + return false + } + if acct, _ := tok.Metadata["auth.google.serviceAccount"].(string); acct != "default" { + return false + } + return true +} + +func isDirectPathXdsUsed(o *Options) bool { + // Method 1: Enable DirectPath xDS by env; + if b, _ := strconv.ParseBool(os.Getenv(enableDirectPathXdsEnvVar)); b { + return true + } + // Method 2: Enable DirectPath xDS by option; + if o.InternalOptions != nil && o.InternalOptions.EnableDirectPathXds { + return true + } + return false +} + +// configureDirectPath returns some dial options and an endpoint to use if the +// configuration allows the use of direct path. If it does not the provided +// grpcOpts and endpoint are returned. +func configureDirectPath(grpcOpts []grpc.DialOption, opts *Options, endpoint string, creds auth.TokenProvider) ([]grpc.DialOption, string) { + if isDirectPathEnabled(endpoint, opts) && metadata.OnGCE() && isTokenProviderDirectPathCompatible(creds, opts) { + // Overwrite all of the previously specific DialOptions, DirectPath uses its own set of credentials and certificates. + grpcOpts = []grpc.DialOption{ + grpc.WithCredentialsBundle(grpcgoogle.NewDefaultCredentialsWithOptions(grpcgoogle.DefaultCredentialsOptions{PerRPCCreds: &grpcTokenProvider{TokenProvider: creds}}))} + if timeoutDialerOption != nil { + grpcOpts = append(grpcOpts, timeoutDialerOption) + } + // Check if google-c2p resolver is enabled for DirectPath + if isDirectPathXdsUsed(opts) { + // google-c2p resolver target must not have a port number + if addr, _, err := net.SplitHostPort(endpoint); err == nil { + endpoint = "google-c2p:///" + addr + } else { + endpoint = "google-c2p:///" + endpoint + } + } else { + if !strings.HasPrefix(endpoint, "dns:///") { + endpoint = "dns:///" + endpoint + } + grpcOpts = append(grpcOpts, + // For now all DirectPath go clients will be using the following lb config, but in future + // when different services need different configs, then we should change this to a + // per-service config. + grpc.WithDisableServiceConfig(), + grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"grpclb":{"childPolicy":[{"pick_first":{}}]}}]}`)) + } + // TODO: add support for system parameters (quota project, request reason) via chained interceptor. + } + return grpcOpts, endpoint +} diff --git a/auth/grpctransport/grpctransport.go b/auth/grpctransport/grpctransport.go new file mode 100644 index 000000000000..59480480ae39 --- /dev/null +++ b/auth/grpctransport/grpctransport.go @@ -0,0 +1,279 @@ +// 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" + "errors" + "fmt" + "net/http" + + "cloud.google.com/go/auth" + "cloud.google.com/go/auth/detect" + "cloud.google.com/go/auth/internal/transport" + "go.opencensus.io/plugin/ocgrpc" + "google.golang.org/grpc" + "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 +) + +// Options used to configure a [GRPCClientConnPool] from [Dial]. +type Options struct { + // DisableTelemetry disables default telemetry (OpenCensus). 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 + // TokenProvider specifies the provider used to add Authorization metadata + // to all requests. If set DetectOpts are ignored. + TokenProvider auth.TokenProvider + // DetectOpts configures settings for detect Application Default + // Credentials. + DetectOpts *detect.Options + + // 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") + } + hasCreds := o.TokenProvider != 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() *detect.Options { + 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 + } + 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 + // DefaultEndpoint specifies the default endpoint. + DefaultEndpoint string + // DefaultMTLSEndpoint specifies the default mTLS endpoint. + DefaultMTLSEndpoint string + // DefaultScopes specifies the default OAuth2 scopes to be used for a + // service. + DefaultScopes []string +} + +// 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, false, 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, + Client: opts.client(), + } + if io := opts.InternalOptions; io != nil { + tOpts.DefaultEndpoint = io.DefaultEndpoint + tOpts.DefaultMTLSEndpoint = io.DefaultMTLSEndpoint + } + 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), + } + + // Authentication can only be sent when communicating over a secure connection. + if !opts.DisableAuthentication { + metadata := opts.Metadata + creds, err := detect.DefaultCredentials(opts.resolveDetectOptions()) + if err != nil { + return nil, err + } + var tp auth.TokenProvider = creds + if opts.TokenProvider != nil { + tp = opts.TokenProvider + } + + qp := creds.QuotaProjectID() + if qp != "" { + if metadata == nil { + metadata = make(map[string]string, 1) + } + metadata[quotaProjectHeaderKey] = qp + } + + grpcOpts = append(grpcOpts, + grpc.WithPerRPCCredentials(&grpcTokenProvider{ + TokenProvider: tp, + metadata: metadata, + }), + ) + + // 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...) +} + +// grpcTokenProvider satisfies https://pkg.go.dev/google.golang.org/grpc/credentials#PerRPCCredentials. +type grpcTokenProvider struct { + auth.TokenProvider + + secure bool + + // Additional metadata attached as headers. + metadata map[string]string +} + +func (tp *grpcTokenProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + token, err := tp.Token(ctx) + if err != nil { + return nil, err + } + if tp.secure { + ri, _ := credentials.RequestInfoFromContext(ctx) + if err = credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil { + return nil, fmt.Errorf("unable to transfer TokenProvider PerRPCCredentials: %v", err) + } + } + metadata := map[string]string{ + "authorization": token.Type + " " + token.Value, + } + for k, v := range tp.metadata { + metadata[k] = v + } + return metadata, nil +} + +func (tp *grpcTokenProvider) RequireTransportSecurity() bool { + return tp.secure +} + +func addOCStatsHandler(dialOpts []grpc.DialOption, opts *Options) []grpc.DialOption { + if opts.DisableTelemetry { + return dialOpts + } + return append(dialOpts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{})) +} diff --git a/auth/grpctransport/grpctransport_test.go b/auth/grpctransport/grpctransport_test.go new file mode 100644 index 000000000000..15f0b1387a0e --- /dev/null +++ b/auth/grpctransport/grpctransport_test.go @@ -0,0 +1,314 @@ +// 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" + "errors" + "net" + "testing" + + "cloud.google.com/go/auth" + "cloud.google.com/go/auth/detect" + echo "cloud.google.com/go/auth/grpctransport/testdata" + "github.com/google/go-cmp/cmp" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" +) + +func TestCheckDirectPathEndPoint(t *testing.T) { + for _, testcase := range []struct { + name string + endpoint string + want bool + }{ + { + name: "empty endpoint are disallowed", + endpoint: "", + want: false, + }, + { + name: "dns schemes are allowed", + endpoint: "dns:///foo", + want: true, + }, + { + name: "host without no prefix are allowed", + endpoint: "foo", + want: true, + }, + { + name: "host with port are allowed", + endpoint: "foo:1234", + want: true, + }, + { + name: "non-dns schemes are disallowed", + endpoint: "https://foo", + want: false, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + if got := checkDirectPathEndPoint(testcase.endpoint); got != testcase.want { + t.Fatalf("got %v, want %v", got, testcase.want) + } + }) + } +} + +func TestDial_FailsValidation(t *testing.T) { + tests := []struct { + name string + opts *Options + }{ + { + name: "missing options", + }, + { + name: "has creds with disable options, tp", + opts: &Options{ + DisableAuthentication: true, + TokenProvider: staticTP("fakeToken"), + }, + }, + { + name: "has creds with disable options, cred file", + opts: &Options{ + DisableAuthentication: true, + DetectOpts: &detect.Options{ + CredentialsFile: "abc.123", + }, + }, + }, + { + name: "has creds with disable options, cred json", + opts: &Options{ + DisableAuthentication: true, + DetectOpts: &detect.Options{ + CredentialsJSON: []byte(`{"foo":"bar"}`), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := Dial(context.Background(), false, tt.opts) + if err == nil { + t.Fatal("NewClient() = _, nil, want error") + } + }) + } +} + +func TestOptions_ResolveDetectOptions(t *testing.T) { + tests := []struct { + name string + in *Options + want *detect.Options + }{ + { + name: "base", + in: &Options{ + DetectOpts: &detect.Options{ + Scopes: []string{"scope"}, + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Scopes: []string{"scope"}, + CredentialsFile: "/path/to/a/file", + }, + }, + { + name: "self-signed, with scope", + in: &Options{ + InternalOptions: &InternalOptions{ + EnableJWTWithScope: true, + }, + DetectOpts: &detect.Options{ + Scopes: []string{"scope"}, + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Scopes: []string{"scope"}, + CredentialsFile: "/path/to/a/file", + UseSelfSignedJWT: true, + }, + }, + { + name: "self-signed, with aud", + in: &Options{ + DetectOpts: &detect.Options{ + Audience: "aud", + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Audience: "aud", + CredentialsFile: "/path/to/a/file", + UseSelfSignedJWT: true, + }, + }, + { + name: "use default scopes", + in: &Options{ + InternalOptions: &InternalOptions{ + DefaultScopes: []string{"default"}, + DefaultAudience: "default", + }, + DetectOpts: &detect.Options{ + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Scopes: []string{"default"}, + CredentialsFile: "/path/to/a/file", + }, + }, + { + name: "don't use default scopes, scope provided", + in: &Options{ + InternalOptions: &InternalOptions{ + DefaultScopes: []string{"default"}, + DefaultAudience: "default", + }, + DetectOpts: &detect.Options{ + Scopes: []string{"non-default"}, + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Scopes: []string{"non-default"}, + CredentialsFile: "/path/to/a/file", + }, + }, + { + name: "don't use default scopes, aud provided", + in: &Options{ + InternalOptions: &InternalOptions{ + DefaultScopes: []string{"default"}, + DefaultAudience: "default", + }, + DetectOpts: &detect.Options{ + Audience: "non-default", + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Audience: "non-default", + CredentialsFile: "/path/to/a/file", + UseSelfSignedJWT: true, + }, + }, + { + name: "use default aud", + in: &Options{ + InternalOptions: &InternalOptions{ + DefaultAudience: "default", + }, + DetectOpts: &detect.Options{ + CredentialsFile: "/path/to/a/file", + }, + }, + want: &detect.Options{ + Audience: "default", + CredentialsFile: "/path/to/a/file", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.in.resolveDetectOptions() + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestNewClient_DetectedServiceAccount(t *testing.T) { + testQuota := "testquota" + wantHeader := "bar" + t.Setenv("GOOGLE_CLOUD_QUOTA_PROJECT", testQuota) + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + } + gsrv := grpc.NewServer() + defer gsrv.Stop() + echo.RegisterEchoerServer(gsrv, &fakeEchoService{ + Fn: func(ctx context.Context, _ *echo.EchoRequest) (*echo.EchoReply, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + t.Error("unable to extract metadata") + return nil, errors.New("oops") + } + if got := md.Get("authorization"); len(got) != 1 { + t.Errorf(`got "", want an auth token`) + } + if got := md.Get("Foo"); len(got) != 1 || got[0] != wantHeader { + t.Errorf("got %q, want %q", got, wantHeader) + } + if got := md.Get(quotaProjectHeaderKey); len(got) != 1 || got[0] != testQuota { + t.Errorf("got %q, want %q", got, testQuota) + } + return &echo.EchoReply{}, nil + }, + }) + go func() { + if err := gsrv.Serve(l); err != nil { + panic(err) + } + }() + + pool, err := Dial(context.Background(), false, &Options{ + Metadata: map[string]string{"Foo": wantHeader}, + InternalOptions: &InternalOptions{ + DefaultEndpoint: l.Addr().String(), + }, + DetectOpts: &detect.Options{ + Audience: l.Addr().String(), + CredentialsFile: "../internal/testdata/sa.json", + UseSelfSignedJWT: true, + }, + GRPCDialOpts: []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, + }) + if err != nil { + t.Fatalf("NewClient() = %v", err) + } + client := echo.NewEchoerClient(pool) + if _, err := client.Echo(context.Background(), &echo.EchoRequest{}); err != nil { + t.Fatalf("client.Echo() = %v", err) + } +} + +type staticTP string + +func (tp staticTP) Token(context.Context) (*auth.Token, error) { + return &auth.Token{ + Value: string(tp), + }, nil +} + +type fakeEchoService struct { + Fn func(context.Context, *echo.EchoRequest) (*echo.EchoReply, error) + echo.UnimplementedEchoerServer +} + +func (s *fakeEchoService) Echo(c context.Context, r *echo.EchoRequest) (*echo.EchoReply, error) { + return s.Fn(c, r) +} diff --git a/auth/grpctransport/pool.go b/auth/grpctransport/pool.go new file mode 100644 index 000000000000..642679f9b76a --- /dev/null +++ b/auth/grpctransport/pool.go @@ -0,0 +1,119 @@ +// 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" + "fmt" + "sync/atomic" + + "google.golang.org/grpc" +) + +// GRPCClientConnPool is an interface that satisfies +// [google.golang.org/grpc.ClientConnInterface] and has some utility functions +// that are needed for connection lifecycle when using in a client library. It +// may be a pool or a single connection. This interface is not intended to, and +// can't be, implemented by others. +type GRPCClientConnPool interface { + // Connection returns a [google.golang.org/grpc.ClientConn] from the pool. + // + // ClientConn aren't returned to the pool and should not be closed directly. + Connection() *grpc.ClientConn + + // Len returns the number of connections in the pool. It will always return + // the same value. + Len() int + + // Close closes every ClientConn in the pool. The error returned by Close + // may be a single error or multiple errors. + Close() error + + grpc.ClientConnInterface + + // private ensure others outside this package can't implement this type + private() +} + +// singleConnPool is a special case for a single connection. +type singleConnPool struct { + *grpc.ClientConn +} + +func (p *singleConnPool) Connection() *grpc.ClientConn { return p.ClientConn } +func (p *singleConnPool) Len() int { return 1 } +func (p *singleConnPool) private() {} + +type roundRobinConnPool struct { + conns []*grpc.ClientConn + + idx uint32 // access via sync/atomic +} + +func (p *roundRobinConnPool) Len() int { + return len(p.conns) +} + +func (p *roundRobinConnPool) Connection() *grpc.ClientConn { + i := atomic.AddUint32(&p.idx, 1) + return p.conns[i%uint32(len(p.conns))] +} + +func (p *roundRobinConnPool) Close() error { + var errs multiError + for _, conn := range p.conns { + if err := conn.Close(); err != nil { + errs = append(errs, err) + } + } + if len(errs) == 0 { + return nil + } + return errs +} + +func (p *roundRobinConnPool) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...grpc.CallOption) error { + return p.Connection().Invoke(ctx, method, args, reply, opts...) +} + +func (p *roundRobinConnPool) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) { + return p.Connection().NewStream(ctx, desc, method, opts...) +} + +func (p *roundRobinConnPool) private() {} + +// multiError represents errors from multiple conns in the group. +type multiError []error + +func (m multiError) Error() string { + s, n := "", 0 + for _, e := range m { + if e != nil { + if n == 0 { + s = e.Error() + } + n++ + } + } + switch n { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, n-1) +} diff --git a/auth/grpctransport/pool_test.go b/auth/grpctransport/pool_test.go new file mode 100644 index 000000000000..df0fc0b72d28 --- /dev/null +++ b/auth/grpctransport/pool_test.go @@ -0,0 +1,147 @@ +// 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" + "errors" + "net" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func TestPool_RoundRobin(t *testing.T) { + conn1 := &grpc.ClientConn{} + conn2 := &grpc.ClientConn{} + + pool := &roundRobinConnPool{ + conns: []*grpc.ClientConn{ + conn1, conn2, + }, + } + + if got := pool.Connection(); got != conn2 { + t.Errorf("pool.Conn() #1 = %v, want conn2 (%v)", got, conn2) + } + if got := pool.Connection(); got != conn1 { + t.Errorf("pool.Conn() #2 = %v, want conn1 (%v)", got, conn1) + } + if got := pool.Connection(); got != conn2 { + t.Errorf("pool.Conn() #3 = %v, want conn2 (%v)", got, conn2) + } + if got := pool.Len(); got != 2 { + t.Errorf("pool.Len() = %v, want %v", got, 2) + } +} + +func TestPool_SingleConn(t *testing.T) { + conn1 := &grpc.ClientConn{} + pool := &singleConnPool{conn1} + + if got := pool.Connection(); got != conn1 { + t.Errorf("pool.Conn() #1 = %v, want conn2 (%v)", got, conn1) + } + if got := pool.Connection(); got != conn1 { + t.Errorf("pool.Conn() #2 = %v, want conn1 (%v)", got, conn1) + } + if got := pool.Len(); got != 1 { + t.Errorf("pool.Len() = %v, want %v", got, 1) + } +} + +func TestClose(t *testing.T) { + _, l := mockServer(t) + + pool := &roundRobinConnPool{} + for i := 0; i < 4; i++ { + conn, err := grpc.Dial(l.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatal(err) + } + pool.conns = append(pool.conns, conn) + } + + if err := pool.Close(); err != nil { + t.Fatalf("pool.Close: %v", err) + } +} + +func TestWithEndpointAndPoolSize(t *testing.T) { + _, l := mockServer(t) + ctx := context.Background() + connPool, err := Dial(ctx, false, &Options{ + Endpoint: l.Addr().String(), + PoolSize: 4, + }) + if err != nil { + t.Fatal(err) + } + + if err := connPool.Close(); err != nil { + t.Fatalf("pool.Close: %v", err) + } +} + +func TestMultiError(t *testing.T) { + tests := []struct { + name string + errs multiError + want string + }{ + { + name: "0 errors", + want: "(0 errors)", + }, + { + name: "1 errors", + errs: []error{errors.New("the full error message")}, + want: "the full error message", + }, + { + name: "2 errors", + errs: []error{errors.New("foo"), errors.New("bar")}, + want: "foo (and 1 other error)", + }, + { + name: "3 errors", + errs: []error{errors.New("foo"), errors.New("bar"), errors.New("baz")}, + want: "foo (and 2 other errors)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.errs.Error() + if got != tt.want { + t.Fatalf("got %v, want %v", got, tt.want) + } + }) + } +} + +func mockServer(t *testing.T) (*grpc.Server, net.Listener) { + t.Helper() + + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatal(err) + } + + s := grpc.NewServer() + go s.Serve(l) + + return s, l +} diff --git a/auth/grpctransport/testdata/README.md b/auth/grpctransport/testdata/README.md new file mode 100644 index 000000000000..7bbe55ebc476 --- /dev/null +++ b/auth/grpctransport/testdata/README.md @@ -0,0 +1,15 @@ +# testdata + +## How to regenerate proto derived files + +Ensure you have installed the following tools: + +- [protoc](https://grpc.io/docs/protoc-installation/) +- [protoc-gen-go](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go) +- [protoc-gen-go-grpc](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc) + +Run the following command from this directory: + +```bash +protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative echo.proto +``` diff --git a/auth/grpctransport/testdata/echo.pb.go b/auth/grpctransport/testdata/echo.pb.go new file mode 100644 index 000000000000..d64b7746ada9 --- /dev/null +++ b/auth/grpctransport/testdata/echo.pb.go @@ -0,0 +1,227 @@ +// 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.24.2 +// source: echo.proto + +package echo + +import ( + reflect "reflect" + sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EchoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *EchoRequest) Reset() { + *x = EchoRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest) ProtoMessage() {} + +func (x *EchoRequest) ProtoReflect() protoreflect.Message { + mi := &file_echo_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_echo_proto_rawDescGZIP(), []int{0} +} + +func (x *EchoRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type EchoReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *EchoReply) Reset() { + *x = EchoReply{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoReply) ProtoMessage() {} + +func (x *EchoReply) ProtoReflect() protoreflect.Message { + mi := &file_echo_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoReply.ProtoReflect.Descriptor instead. +func (*EchoReply) Descriptor() ([]byte, []int) { + return file_echo_proto_rawDescGZIP(), []int{1} +} + +func (x *EchoReply) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_echo_proto protoreflect.FileDescriptor + +var file_echo_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x65, 0x63, + 0x68, 0x6f, 0x22, 0x27, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x25, 0x0a, 0x09, 0x45, + 0x63, 0x68, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x32, 0x36, 0x0a, 0x06, 0x45, 0x63, 0x68, 0x6f, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x04, + 0x45, 0x63, 0x68, 0x6f, 0x12, 0x11, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x65, 0x63, 0x68, 0x6f, 0x2e, 0x45, + 0x63, 0x68, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x36, 0x5a, 0x34, 0x63, 0x6c, + 0x6f, 0x75, 0x64, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, + 0x6f, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x72, 0x6f, 0x74, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x64, 0x61, 0x74, 0x61, 0x3b, 0x65, 0x63, + 0x68, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_echo_proto_rawDescOnce sync.Once + file_echo_proto_rawDescData = file_echo_proto_rawDesc +) + +func file_echo_proto_rawDescGZIP() []byte { + file_echo_proto_rawDescOnce.Do(func() { + file_echo_proto_rawDescData = protoimpl.X.CompressGZIP(file_echo_proto_rawDescData) + }) + return file_echo_proto_rawDescData +} + +var file_echo_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_echo_proto_goTypes = []interface{}{ + (*EchoRequest)(nil), // 0: echo.EchoRequest + (*EchoReply)(nil), // 1: echo.EchoReply +} +var file_echo_proto_depIdxs = []int32{ + 0, // 0: echo.Echoer.Echo:input_type -> echo.EchoRequest + 1, // 1: echo.Echoer.Echo:output_type -> echo.EchoReply + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_echo_proto_init() } +func file_echo_proto_init() { + if File_echo_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_echo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_echo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_echo_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_echo_proto_goTypes, + DependencyIndexes: file_echo_proto_depIdxs, + MessageInfos: file_echo_proto_msgTypes, + }.Build() + File_echo_proto = out.File + file_echo_proto_rawDesc = nil + file_echo_proto_goTypes = nil + file_echo_proto_depIdxs = nil +} diff --git a/auth/grpctransport/testdata/echo.proto b/auth/grpctransport/testdata/echo.proto new file mode 100644 index 000000000000..18c709943f6e --- /dev/null +++ b/auth/grpctransport/testdata/echo.proto @@ -0,0 +1,31 @@ +// 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. + +syntax = "proto3"; + +package echo; + +option go_package = "cloud.google.com/go/auth/grpctransprot/testdata;echo"; + +service Echoer { + rpc Echo (EchoRequest) returns (EchoReply) {} + } + + message EchoRequest { + string message = 1; + } + + message EchoReply { + string message = 1; + } \ No newline at end of file diff --git a/auth/grpctransport/testdata/echo_grpc.pb.go b/auth/grpctransport/testdata/echo_grpc.pb.go new file mode 100644 index 000000000000..0e863e5bf882 --- /dev/null +++ b/auth/grpctransport/testdata/echo_grpc.pb.go @@ -0,0 +1,124 @@ +// 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. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.24.2 +// source: echo.proto + +package echo + +import ( + context "context" + + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Echoer_Echo_FullMethodName = "/echo.Echoer/Echo" +) + +// EchoerClient is the client API for Echoer service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type EchoerClient interface { + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoReply, error) +} + +type echoerClient struct { + cc grpc.ClientConnInterface +} + +func NewEchoerClient(cc grpc.ClientConnInterface) EchoerClient { + return &echoerClient{cc} +} + +func (c *echoerClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoReply, error) { + out := new(EchoReply) + err := c.cc.Invoke(ctx, Echoer_Echo_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EchoerServer is the server API for Echoer service. +// All implementations must embed UnimplementedEchoerServer +// for forward compatibility +type EchoerServer interface { + Echo(context.Context, *EchoRequest) (*EchoReply, error) + mustEmbedUnimplementedEchoerServer() +} + +// UnimplementedEchoerServer must be embedded to have forward compatible implementations. +type UnimplementedEchoerServer struct { +} + +func (UnimplementedEchoerServer) Echo(context.Context, *EchoRequest) (*EchoReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedEchoerServer) mustEmbedUnimplementedEchoerServer() {} + +// UnsafeEchoerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EchoerServer will +// result in compilation errors. +type UnsafeEchoerServer interface { + mustEmbedUnimplementedEchoerServer() +} + +func RegisterEchoerServer(s grpc.ServiceRegistrar, srv EchoerServer) { + s.RegisterService(&Echoer_ServiceDesc, srv) +} + +func _Echoer_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EchoerServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Echoer_Echo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoerServer).Echo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Echoer_ServiceDesc is the grpc.ServiceDesc for Echoer service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Echoer_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "echo.Echoer", + HandlerType: (*EchoerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _Echoer_Echo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "echo.proto", +} diff --git a/auth/impersonate/integration_test.go b/auth/impersonate/integration_test.go index 337eb0ff9e8c..72ce4f5aa9e9 100644 --- a/auth/impersonate/integration_test.go +++ b/auth/impersonate/integration_test.go @@ -28,7 +28,6 @@ import ( "cloud.google.com/go/auth/impersonate" "cloud.google.com/go/auth/internal/testutil" "cloud.google.com/go/auth/internal/testutil/testgcs" - "google.golang.org/api/idtoken" ) const ( @@ -118,61 +117,63 @@ func TestCredentialsTokenSourceIntegration(t *testing.T) { } } -func TestIDTokenSourceIntegration(t *testing.T) { - testutil.IntegrationTestCheck(t) +// TODO(codyoss): uncomment in #8580 - ctx := context.Background() - tests := []struct { - name string - baseKeyFile string - delegates []string - }{ - { - name: "SA -> SA", - baseKeyFile: readerKeyFile, - }, - { - name: "SA -> Delegate -> SA", - baseKeyFile: baseKeyFile, - delegates: []string{readerEmail}, - }, - } +// func TestIDTokenSourceIntegration(t *testing.T) { +// testutil.IntegrationTestCheck(t) - for _, tt := range tests { - name := tt.name - t.Run(name, func(t *testing.T) { - creds, err := detect.DefaultCredentials(&detect.Options{ - Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, - CredentialsFile: tt.baseKeyFile, - }) - if err != nil { - t.Fatalf("detect.DefaultCredentials() = %v", err) - } - aud := "http://example.com/" - tp, err := impersonate.NewIDTokenProvider(&impersonate.IDTokenOptions{ - TargetPrincipal: writerEmail, - Audience: aud, - Delegates: tt.delegates, - IncludeEmail: true, - TokenProvider: creds, - }) - if err != nil { - t.Fatalf("failed to create ts: %v", err) - } - tok, err := tp.Token(ctx) - if err != nil { - t.Fatalf("unable to retrieve Token: %v", err) - } - validTok, err := idtoken.Validate(ctx, tok.Value, aud) - if err != nil { - t.Fatalf("token validation failed: %v", err) - } - if validTok.Audience != aud { - t.Fatalf("got %q, want %q", validTok.Audience, aud) - } - if validTok.Claims["email"] != writerEmail { - t.Fatalf("got %q, want %q", validTok.Claims["email"], writerEmail) - } - }) - } -} +// ctx := context.Background() +// tests := []struct { +// name string +// baseKeyFile string +// delegates []string +// }{ +// { +// name: "SA -> SA", +// baseKeyFile: readerKeyFile, +// }, +// { +// name: "SA -> Delegate -> SA", +// baseKeyFile: baseKeyFile, +// delegates: []string{readerEmail}, +// }, +// } + +// for _, tt := range tests { +// name := tt.name +// t.Run(name, func(t *testing.T) { +// creds, err := detect.DefaultCredentials(&detect.Options{ +// Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, +// CredentialsFile: tt.baseKeyFile, +// }) +// if err != nil { +// t.Fatalf("detect.DefaultCredentials() = %v", err) +// } +// aud := "http://example.com/" +// tp, err := impersonate.NewIDTokenProvider(&impersonate.IDTokenOptions{ +// TargetPrincipal: writerEmail, +// Audience: aud, +// Delegates: tt.delegates, +// IncludeEmail: true, +// TokenProvider: creds, +// }) +// if err != nil { +// t.Fatalf("failed to create ts: %v", err) +// } +// tok, err := tp.Token(ctx) +// if err != nil { +// t.Fatalf("unable to retrieve Token: %v", err) +// } +// validTok, err := idtoken.Validate(ctx, tok.Value, aud) +// if err != nil { +// t.Fatalf("token validation failed: %v", err) +// } +// if validTok.Audience != aud { +// t.Fatalf("got %q, want %q", validTok.Audience, aud) +// } +// if validTok.Claims["email"] != writerEmail { +// t.Fatalf("got %q, want %q", validTok.Claims["email"], writerEmail) +// } +// }) +// } +// } diff --git a/auth/internal/transport/cba.go b/auth/internal/transport/cba.go index 6f9d51976d38..2ebaea824519 100644 --- a/auth/internal/transport/cba.go +++ b/auth/internal/transport/cba.go @@ -57,9 +57,10 @@ type Options struct { Client *http.Client } -// GetGRPCTransportConfigAndEndpoint returns an instance of credentials.TransportCredentials, and the +// GetGRPCTransportCredsAndEndpoint returns an instance of +// [google.golang.org/grpc/credentials.TransportCredentials], and the // corresponding endpoint to use for GRPC client. -func GetGRPCTransportConfigAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) { +func GetGRPCTransportCredsAndEndpoint(opts *Options) (credentials.TransportCredentials, string, error) { config, err := getTransportConfig(opts) if err != nil { return nil, "", err @@ -91,8 +92,8 @@ func GetGRPCTransportConfigAndEndpoint(opts *Options) (credentials.TransportCred return s2aTransportCreds, config.s2aMTLSEndpoint, nil } -// GetHTTPTransportConfig returns a client certificate source, a function for dialing MTLS with S2A, -// and the endpoint to use for HTTP client. +// GetHTTPTransportConfig returns a client certificate source and a function for +// dialing MTLS with S2A. func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) { config, err := getTransportConfig(opts) if err != nil { diff --git a/auth/internal/transport/cba_test.go b/auth/internal/transport/cba_test.go index b03f77ac88d9..509bc18e6639 100644 --- a/auth/internal/transport/cba_test.go +++ b/auth/internal/transport/cba_test.go @@ -241,7 +241,7 @@ func TestGetGRPCTransportConfigAndEndpoint(t *testing.T) { } else { t.Setenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") } - _, endpoint, _ := GetGRPCTransportConfigAndEndpoint(tc.opts) + _, endpoint, _ := GetGRPCTransportCredsAndEndpoint(tc.opts) if tc.want != endpoint { t.Fatalf("%s: want endpoint: [%s], got [%s]", tc.name, tc.want, endpoint) } diff --git a/auth/oauth2adapt/go.sum b/auth/oauth2adapt/go.sum index c5b8ac00661d..196c39194511 100644 --- a/auth/oauth2adapt/go.sum +++ b/auth/oauth2adapt/go.sum @@ -1,3 +1,6 @@ +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -5,19 +8,27 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=