snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
306
internal/xds/grpcservice/grpcservice.go
Normal file
306
internal/xds/grpcservice/grpcservice.go
Normal file
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 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.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package grpcservice parses and validates envoy GrpcService protos into a
|
||||
// form usable for creating side-channel gRPC connections.
|
||||
package grpcservice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
imetadata "google.golang.org/grpc/internal/metadata"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdscreds "google.golang.org/grpc/internal/xds/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/resolver"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
const (
|
||||
maxHeaderKeyLen = 16384
|
||||
maxHeaderValueLen = 16384
|
||||
)
|
||||
|
||||
// Config is the parsed form of a GrpcService proto. It carries the built,
|
||||
// ready-to-use credentials for the side channel.
|
||||
type Config struct {
|
||||
// TargetURI is the gRPC target URI of the side-channel service.
|
||||
TargetURI string
|
||||
// Timeout, if non-zero, is the deadline to use for RPCs on the side
|
||||
// channel.
|
||||
Timeout time.Duration
|
||||
// InitialMetadata is the metadata to add to RPCs on the side channel.
|
||||
InitialMetadata metadata.MD
|
||||
// ChannelCredentials are the channel credentials to create the side
|
||||
// channel with, paired with the identity of their source configuration.
|
||||
ChannelCredentials *xdscreds.ChannelCreds
|
||||
// CallCredentials are the call credentials to apply to RPCs sent on the
|
||||
// side channel, paired with the identities of their source
|
||||
// configurations, preserving order.
|
||||
CallCredentials []*xdscreds.CallCreds
|
||||
}
|
||||
|
||||
// Equal reports whether c and other are equal.
|
||||
func (c *Config) Equal(other *Config) bool {
|
||||
if c == nil || other == nil {
|
||||
return c == other
|
||||
}
|
||||
targetEqual := c.TargetURI == other.TargetURI
|
||||
timeoutEqual := c.Timeout == other.Timeout
|
||||
metadataEqual := maps.EqualFunc(c.InitialMetadata, other.InitialMetadata, slices.Equal)
|
||||
channelCredsEqual := c.ChannelCredentials.Equal(other.ChannelCredentials)
|
||||
callCredsEqual := slices.EqualFunc(c.CallCredentials, other.CallCredentials, (*xdscreds.CallCreds).Equal)
|
||||
return targetEqual && timeoutEqual && metadataEqual && channelCredsEqual && callCredsEqual
|
||||
}
|
||||
|
||||
// Close releases the credentials owned by the config. It is idempotent, and a
|
||||
// no-op for credentials owned by another component (e.g. the allowlisted
|
||||
// credentials owned by the bootstrap config).
|
||||
func (c *Config) Close() {
|
||||
c.ChannelCredentials.Close()
|
||||
for _, cc := range c.CallCredentials {
|
||||
cc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Dial creates a channel to the side-channel service, using the channel and
|
||||
// call credentials from the config along with the provided dial options.
|
||||
//
|
||||
// Dial does not take ownership of the config: the caller releases the
|
||||
// config's credentials via Close when the config is no longer needed, after
|
||||
// closing any channel created from it.
|
||||
func (c *Config) Dial(opts ...grpc.DialOption) (*grpc.ClientConn, error) {
|
||||
// The credentials are appended after the provided options so that they
|
||||
// cannot be overridden.
|
||||
dialOpts := make([]grpc.DialOption, 0, len(opts)+len(c.CallCredentials)+1)
|
||||
dialOpts = append(dialOpts, opts...)
|
||||
dialOpts = append(dialOpts, grpc.WithCredentialsBundle(c.ChannelCredentials.Bundle()))
|
||||
for _, cc := range c.CallCredentials {
|
||||
dialOpts = append(dialOpts, grpc.WithPerRPCCredentials(cc.Credentials()))
|
||||
}
|
||||
return grpc.NewClient(c.TargetURI, dialOpts...)
|
||||
}
|
||||
|
||||
// Parse parses and validates a GrpcService proto into a Config, applying the
|
||||
// gRFC A102 trust policy. bc is the bootstrap configuration of the xDS client
|
||||
// that received the resource, and sc is the configuration of the management
|
||||
// server that sent it; both must be non-nil.
|
||||
//
|
||||
// Credentials configured in the proto are honored only when the delivering
|
||||
// management server is trusted, i.e. configured with the trusted_xds_server
|
||||
// server feature. For untrusted management servers the target must be present
|
||||
// in the bootstrap allowed_grpc_services map, and the returned Config carries
|
||||
// the credentials configured there.
|
||||
//
|
||||
// The credentials in the returned Config are built and ready to use. The
|
||||
// caller owns the Config and releases its credentials via Close when it is no
|
||||
// longer needed, after closing any channel dialed from it.
|
||||
func Parse(gs *v3corepb.GrpcService, bc *bootstrap.Config, sc *bootstrap.ServerConfig) (*Config, error) {
|
||||
googleGrpc := gs.GetGoogleGrpc()
|
||||
if googleGrpc == nil {
|
||||
return nil, fmt.Errorf("grpcservice: only google_grpc GrpcService config is supported")
|
||||
}
|
||||
|
||||
targetURI := googleGrpc.GetTargetUri()
|
||||
if targetURI == "" {
|
||||
return nil, fmt.Errorf("grpcservice: target_uri must be non-empty")
|
||||
}
|
||||
if err := validateTargetURI(targetURI); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{TargetURI: targetURI}
|
||||
var err error
|
||||
if cfg.Timeout, err = parseTimeout(gs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.InitialMetadata, err = parseInitialMetadata(gs.GetInitialMetadata()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Credentials are built last, so that no error path can drop built
|
||||
// credentials.
|
||||
if sc.ServerFeaturesTrustedXDSServer() {
|
||||
if cfg.ChannelCredentials, err = buildChannelCredentials(googleGrpc.GetChannelCredentialsPlugin(), bc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.CallCredentials, err = buildCallCredentials(googleGrpc.GetCallCredentialsPlugin()); err != nil {
|
||||
cfg.ChannelCredentials.Close()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
svc := bc.AllowedGRPCService(targetURI)
|
||||
if svc == nil {
|
||||
return nil, fmt.Errorf("grpcservice: target_uri %q is not present in allowed_grpc_services", targetURI)
|
||||
}
|
||||
// The allowlisted credentials are owned by the bootstrap config:
|
||||
// their pairs carry no cleanup, so Config.Close does not affect
|
||||
// them.
|
||||
cfg.ChannelCredentials, cfg.CallCredentials = svc.SideChannelCredentials()
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// buildChannelCredentials builds the first channel credential from the plugin
|
||||
// list whose proto type has a registered builder. It is an error if none of
|
||||
// the configured plugins are supported, or if building the supported plugin
|
||||
// fails.
|
||||
func buildChannelCredentials(plugins []*anypb.Any, bc *bootstrap.Config) (*xdscreds.ChannelCreds, error) {
|
||||
for _, p := range plugins {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
b := xdscreds.GetChannelCredsBuilder(p.GetTypeUrl())
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
bundle, cleanup, err := b(p, bc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpcservice: failed to build channel credentials %q: %v", p.GetTypeUrl(), err)
|
||||
}
|
||||
identity := xdscreds.Identity{Type: p.GetTypeUrl(), Data: p.GetValue()}
|
||||
return xdscreds.NewChannelCreds(bundle, identity, cleanup), nil
|
||||
}
|
||||
return nil, fmt.Errorf("grpcservice: no supported channel credentials found in grpc_service")
|
||||
}
|
||||
|
||||
// buildCallCredentials builds the call credentials from the plugin list,
|
||||
// preserving order. Plugins whose proto type has no registered builder are
|
||||
// skipped; call credentials are optional, so an empty result is not an error.
|
||||
func buildCallCredentials(plugins []*anypb.Any) (_ []*xdscreds.CallCreds, err error) {
|
||||
var out []*xdscreds.CallCreds
|
||||
// Release any credentials built before a mid-iteration failure.
|
||||
defer func() {
|
||||
if err != nil {
|
||||
for _, cc := range out {
|
||||
cc.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
for _, p := range plugins {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
b := xdscreds.GetCallCredsBuilder(p.GetTypeUrl())
|
||||
if b == nil {
|
||||
continue
|
||||
}
|
||||
cc, cleanup, err := b(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpcservice: failed to build call credentials %q: %v", p.GetTypeUrl(), err)
|
||||
}
|
||||
identity := xdscreds.Identity{Type: p.GetTypeUrl(), Data: p.GetValue()}
|
||||
out = append(out, xdscreds.NewCallCreds(cc, identity, cleanup))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// validateTargetURI verifies that the target URI can be handled by a
|
||||
// registered resolver.
|
||||
func validateTargetURI(targetURI string) error {
|
||||
// Mirror the scheme resolution performed by grpc.NewClient: use the
|
||||
// target's scheme if it parses and is registered; otherwise fall back
|
||||
// to the default scheme with the whole target as the endpoint.
|
||||
if u, err := url.Parse(targetURI); err == nil && resolver.Get(u.Scheme) != nil {
|
||||
return nil
|
||||
}
|
||||
canonicalTarget := resolver.GetDefaultScheme() + ":///" + targetURI
|
||||
u, err := url.Parse(canonicalTarget)
|
||||
if err != nil {
|
||||
return fmt.Errorf("grpcservice: target_uri %q is invalid: %v", targetURI, err)
|
||||
}
|
||||
if resolver.Get(u.Scheme) == nil {
|
||||
return fmt.Errorf("grpcservice: no resolver for default scheme %q", u.Scheme)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTimeout validates and converts the GrpcService timeout. A zero timeout
|
||||
// (unset) is allowed; any set value must be strictly positive.
|
||||
func parseTimeout(gs *v3corepb.GrpcService) (time.Duration, error) {
|
||||
d := gs.GetTimeout()
|
||||
if d == nil {
|
||||
return 0, nil
|
||||
}
|
||||
if err := d.CheckValid(); err != nil {
|
||||
return 0, fmt.Errorf("grpcservice: invalid timeout: %v", err)
|
||||
}
|
||||
timeout := d.AsDuration()
|
||||
if timeout <= 0 {
|
||||
return 0, fmt.Errorf("grpcservice: timeout must be strictly positive, got %v", timeout)
|
||||
}
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
// parseInitialMetadata validates the HeaderValue protos and returns them as
|
||||
// metadata.MD, preserving the control-plane order for each key.
|
||||
func parseInitialMetadata(headers []*v3corepb.HeaderValue) (metadata.MD, error) {
|
||||
if len(headers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
md := metadata.MD{}
|
||||
for _, h := range headers {
|
||||
key := h.GetKey()
|
||||
// raw_value takes precedence over the legacy value field.
|
||||
val := h.GetValue()
|
||||
if len(h.GetRawValue()) > 0 {
|
||||
val = string(h.GetRawValue())
|
||||
}
|
||||
if err := validateHeaderKey(key); err != nil {
|
||||
return nil, fmt.Errorf("grpcservice: invalid header key %q: %v", key, err)
|
||||
}
|
||||
if err := validateHeaderValue(key, val); err != nil {
|
||||
return nil, fmt.Errorf("grpcservice: invalid value for header key %q: %v", key, err)
|
||||
}
|
||||
md.Append(key, val)
|
||||
}
|
||||
return md, nil
|
||||
}
|
||||
|
||||
func validateHeaderKey(key string) error {
|
||||
switch {
|
||||
case len(key) > maxHeaderKeyLen:
|
||||
return fmt.Errorf("header key exceeds maximum allowed length of %d", maxHeaderKeyLen)
|
||||
case key == "host":
|
||||
return fmt.Errorf("header key cannot be %q", "host")
|
||||
case strings.HasPrefix(key, ":"):
|
||||
// imetadata.ValidateKey ignores pseudo-headers, but gRFC A102
|
||||
// requires them to be rejected.
|
||||
return fmt.Errorf("header key cannot start with %q", ":")
|
||||
case strings.HasPrefix(key, "grpc-"):
|
||||
return fmt.Errorf("header key cannot start with %q", "grpc-")
|
||||
}
|
||||
return imetadata.ValidateKey(key)
|
||||
}
|
||||
|
||||
func validateHeaderValue(key, val string) error {
|
||||
if len(val) > maxHeaderValueLen {
|
||||
return fmt.Errorf("header value exceeds maximum allowed length of %d", maxHeaderValueLen)
|
||||
}
|
||||
// ValidatePair skips value validation for "-bin" keys, which may carry
|
||||
// arbitrary bytes.
|
||||
return imetadata.ValidatePair(key, val)
|
||||
}
|
||||
462
internal/xds/grpcservice/grpcservice_test.go
Normal file
462
internal/xds/grpcservice/grpcservice_test.go
Normal file
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2026 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package grpcservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/xds/bootstrap"
|
||||
xdscreds "google.golang.org/grpc/internal/xds/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/testdata"
|
||||
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||
accesstokenpb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/call_credentials/access_token/v3"
|
||||
tlscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/tls/v3"
|
||||
xdscredspb "github.com/envoyproxy/go-control-plane/envoy/extensions/grpc_service/channel_credentials/xds/v3"
|
||||
v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const (
|
||||
target = "dns:///my-service:443"
|
||||
|
||||
insecureCredsTypeURL = "type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials"
|
||||
|
||||
defaultTestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// bootstrapConfig builds a bootstrap Config whose allowed_grpc_services is set
|
||||
// to the provided JSON (a map from target URI to allowed service config).
|
||||
func bootstrapConfig(t *testing.T, allowed string) *bootstrap.Config {
|
||||
t.Helper()
|
||||
// The allowed_grpc_services bootstrap field is parsed only when a
|
||||
// consuming feature is enabled.
|
||||
testutils.SetEnvConfig(t, &envconfig.XDSClientExtProcEnabled, true)
|
||||
contents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: json.RawMessage(`[{"server_uri":"td.googleapis.com:443","channel_creds":[{"type":"insecure"}]}]`),
|
||||
Node: json.RawMessage(`{}`),
|
||||
AllowedGRPCServices: json.RawMessage(allowed),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewContentsForTesting() failed: %v", err)
|
||||
}
|
||||
cfg, err := bootstrap.NewConfigFromContents(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigFromContents() failed: %v", err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// trustedServerConfig returns a server config carrying the trusted_xds_server
|
||||
// server feature.
|
||||
func trustedServerConfig(t *testing.T) *bootstrap.ServerConfig {
|
||||
t.Helper()
|
||||
sc, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{
|
||||
URI: "trusted-server:443",
|
||||
ServerFeatures: []string{"trusted_xds_server"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ServerConfigForTesting() failed: %v", err)
|
||||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
// untrustedServerConfig returns a server config without the
|
||||
// trusted_xds_server server feature.
|
||||
func untrustedServerConfig(t *testing.T) *bootstrap.ServerConfig {
|
||||
t.Helper()
|
||||
sc, err := bootstrap.ServerConfigForTesting(bootstrap.ServerConfigTestingOptions{URI: "untrusted-server:443"})
|
||||
if err != nil {
|
||||
t.Fatalf("ServerConfigForTesting() failed: %v", err)
|
||||
}
|
||||
return sc
|
||||
}
|
||||
|
||||
func googleGrpcService(target string, channelPlugins, callPlugins []*anypb.Any, timeout *durationpb.Duration) *v3corepb.GrpcService {
|
||||
return &v3corepb.GrpcService{
|
||||
TargetSpecifier: &v3corepb.GrpcService_GoogleGrpc_{
|
||||
GoogleGrpc: &v3corepb.GrpcService_GoogleGrpc{
|
||||
TargetUri: target,
|
||||
ChannelCredentialsPlugin: channelPlugins,
|
||||
CallCredentialsPlugin: callPlugins,
|
||||
},
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// protoIdentity returns the identity of a proto plugin config.
|
||||
func protoIdentity(a *anypb.Any) xdscreds.Identity {
|
||||
return xdscreds.Identity{Type: a.GetTypeUrl(), Data: a.GetValue()}
|
||||
}
|
||||
|
||||
func accessTokenPlugin(t *testing.T, token string) *anypb.Any {
|
||||
t.Helper()
|
||||
a, err := anypb.New(&accesstokenpb.AccessTokenCredentials{Token: token})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal AccessTokenCredentials: %v", err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (s) TestParse(t *testing.T) {
|
||||
insecurePlugin := &anypb.Any{TypeUrl: insecureCredsTypeURL}
|
||||
xdsWithInsecureFallback, err := anypb.New(&xdscredspb.XdsCredentials{FallbackCredentials: insecurePlugin})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal XdsCredentials: %v", err)
|
||||
}
|
||||
tokenPlugin := accessTokenPlugin(t, "test-token")
|
||||
unsupportedCallPlugin := &anypb.Any{TypeUrl: "type.googleapis.com/unsupported.CallCredentials"}
|
||||
allowedInsecure := `{"dns:///my-service:443":{"channel_creds":[{"type":"insecure"}]}}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
gs *v3corepb.GrpcService
|
||||
sc *bootstrap.ServerConfig
|
||||
config *bootstrap.Config
|
||||
// want carries the expected target and credential identities;
|
||||
// comparisons use Config.Equal via cmp.Diff.
|
||||
want *Config
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "trusted_insecure_channel_creds",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, nil, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
want: &Config{TargetURI: target, ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil)},
|
||||
},
|
||||
{
|
||||
name: "trusted_xds_creds_resolve_to_fallback",
|
||||
gs: googleGrpcService(target, []*anypb.Any{xdsWithInsecureFallback}, nil, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
want: &Config{TargetURI: target, ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(xdsWithInsecureFallback), nil)},
|
||||
},
|
||||
{
|
||||
// Call-creds plugins of unsupported types are skipped, while
|
||||
// supported ones are built in order.
|
||||
name: "trusted_with_call_creds",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, []*anypb.Any{tokenPlugin, unsupportedCallPlugin}, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
want: &Config{
|
||||
TargetURI: target,
|
||||
ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil),
|
||||
CallCredentials: []*xdscreds.CallCreds{xdscreds.NewCallCreds(nil, protoIdentity(tokenPlugin), nil)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trusted_empty_call_creds_token",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, []*anypb.Any{accessTokenPlugin(t, "")}, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "access token must be non-empty",
|
||||
},
|
||||
{
|
||||
name: "trusted_no_supported_channel_creds",
|
||||
gs: googleGrpcService(target, nil, nil, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "no supported channel credentials",
|
||||
},
|
||||
{
|
||||
name: "untrusted_allowlisted_uses_allowlist_creds",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, nil, nil),
|
||||
sc: untrustedServerConfig(t),
|
||||
config: bootstrapConfig(t, allowedInsecure),
|
||||
want: &Config{TargetURI: target, ChannelCredentials: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil)},
|
||||
},
|
||||
{
|
||||
// The proto's call-creds plugins are ignored along with its
|
||||
// channel-creds plugins: the allowlist's call creds are used.
|
||||
name: "untrusted_allowlisted_uses_allowlist_call_creds",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, []*anypb.Any{tokenPlugin}, nil),
|
||||
sc: untrustedServerConfig(t),
|
||||
config: bootstrapConfig(t, `{"dns:///my-service:443":{"channel_creds":[{"type":"insecure"}],"call_creds":[{"type":"jwt_token_file","config":{"jwt_token_file":"/tokens/token"}}]}}`),
|
||||
want: &Config{
|
||||
TargetURI: target,
|
||||
ChannelCredentials: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
|
||||
CallCredentials: []*xdscreds.CallCreds{xdscreds.NewCallCreds(nil, xdscreds.Identity{
|
||||
Type: "jwt_token_file",
|
||||
// Bootstrap parsing normalizes the configuration JSON
|
||||
// via json.Indent, and the identity carries the
|
||||
// normalized bytes.
|
||||
Data: json.RawMessage("{\n\"jwt_token_file\": \"/tokens/token\"\n}"),
|
||||
}, nil)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "untrusted_not_allowlisted",
|
||||
gs: googleGrpcService(target, nil, nil, nil),
|
||||
sc: untrustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "not present in allowed_grpc_services",
|
||||
},
|
||||
{
|
||||
name: "missing_google_grpc",
|
||||
gs: &v3corepb.GrpcService{},
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "only google_grpc",
|
||||
},
|
||||
{
|
||||
name: "empty_target_uri",
|
||||
gs: googleGrpcService("", nil, nil, nil),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "target_uri must be non-empty",
|
||||
},
|
||||
{
|
||||
name: "valid_timeout",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, nil, durationpb.New(10*time.Second)),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
want: &Config{
|
||||
TargetURI: target,
|
||||
Timeout: 10 * time.Second,
|
||||
ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero_timeout_rejected",
|
||||
gs: googleGrpcService(target, []*anypb.Any{insecurePlugin}, nil, durationpb.New(0)),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
wantErr: "timeout must be strictly positive",
|
||||
},
|
||||
{
|
||||
name: "initial_metadata",
|
||||
gs: func() *v3corepb.GrpcService {
|
||||
gs := googleGrpcService(target, []*anypb.Any{insecurePlugin}, nil, nil)
|
||||
gs.InitialMetadata = []*v3corepb.HeaderValue{
|
||||
{Key: "key-b", Value: "b"},
|
||||
// raw_value takes precedence over the legacy value field.
|
||||
{Key: "key-a", Value: "legacy", RawValue: []byte("raw-a")},
|
||||
}
|
||||
return gs
|
||||
}(),
|
||||
sc: trustedServerConfig(t),
|
||||
config: bootstrapConfig(t, "{}"),
|
||||
want: &Config{
|
||||
TargetURI: target,
|
||||
InitialMetadata: metadata.MD{"key-b": {"b"}, "key-a": {"raw-a"}},
|
||||
ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := Parse(test.gs, test.config, test.sc)
|
||||
if test.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
|
||||
t.Fatalf("Parse() error = %v, want substring %q", err, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() returned unexpected error: %v", err)
|
||||
}
|
||||
// The cmp.Diff below compares credentials by identity only, so it
|
||||
// cannot tell a built credential from a nil one; verify the
|
||||
// credentials were built.
|
||||
if got.ChannelCredentials.Bundle() == nil {
|
||||
t.Error("Parse() returned channel credentials without a built bundle")
|
||||
}
|
||||
for i, cc := range got.CallCredentials {
|
||||
if cc.Credentials() == nil {
|
||||
t.Errorf("Parse() call credentials[%d] have no built credentials", i)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(test.want, got); diff != "" {
|
||||
t.Errorf("Parse() returned unexpected config (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that Dial applies the config's credentials to the created channel:
|
||||
// call credentials that require transport security combined with insecure
|
||||
// channel credentials must fail channel creation.
|
||||
func (s) TestConfigDial(t *testing.T) {
|
||||
gs := googleGrpcService(target, []*anypb.Any{{TypeUrl: insecureCredsTypeURL}}, []*anypb.Any{accessTokenPlugin(t, "test-token")}, nil)
|
||||
cfg, err := Parse(gs, bootstrapConfig(t, "{}"), trustedServerConfig(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() returned unexpected error: %v", err)
|
||||
}
|
||||
defer cfg.Close()
|
||||
if _, err := cfg.Dial(); err == nil || !strings.Contains(err.Error(), "transport level security") {
|
||||
t.Fatalf("Dial() returned error %v, want transport security error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that a Config carrying TLS channel credentials and access token call
|
||||
// credentials, built from a trusted server's GrpcService proto, can dial a
|
||||
// TLS-serving backend, and that RPCs on the resulting channel succeed and
|
||||
// carry the token.
|
||||
func (s) TestConfigDialTLS(t *testing.T) {
|
||||
// Start a TLS-serving backend that verifies the access token arrives.
|
||||
const wantAuthorization = "Bearer test-token"
|
||||
serverCreds, err := credentials.NewServerTLSFromFile(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create server TLS credentials: %v", err)
|
||||
}
|
||||
backend := stubserver.StartTestService(t, &stubserver.StubServer{
|
||||
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no incoming metadata")
|
||||
}
|
||||
if got := md.Get("authorization"); len(got) != 1 || got[0] != wantAuthorization {
|
||||
return nil, fmt.Errorf("authorization metadata = %v, want [%q]", got, wantAuthorization)
|
||||
}
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}, grpc.Creds(serverCreds))
|
||||
defer backend.Stop()
|
||||
|
||||
// Bootstrap configuration with a certificate provider instance watching
|
||||
// the CA certificate that signed the backend's certificate.
|
||||
contents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
|
||||
Servers: json.RawMessage(`[{"server_uri":"td.googleapis.com:443","channel_creds":[{"type":"insecure"}]}]`),
|
||||
Node: json.RawMessage(`{}`),
|
||||
CertificateProviders: map[string]json.RawMessage{
|
||||
"root-instance": json.RawMessage(fmt.Sprintf(`{
|
||||
"plugin_name": "file_watcher",
|
||||
"config": {"ca_certificate_file": %q}
|
||||
}`, testdata.Path("x509/server_ca_cert.pem"))),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewContentsForTesting() failed: %v", err)
|
||||
}
|
||||
bc, err := bootstrap.NewConfigFromContents(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConfigFromContents() failed: %v", err)
|
||||
}
|
||||
|
||||
tlsPlugin, err := anypb.New(&tlscredspb.TlsCredentials{
|
||||
RootCertificateProvider: &v3tlspb.CommonTlsContext_CertificateProviderInstance{InstanceName: "root-instance"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal TlsCredentials: %v", err)
|
||||
}
|
||||
gs := googleGrpcService(backend.Address, []*anypb.Any{tlsPlugin}, []*anypb.Any{accessTokenPlugin(t, "test-token")}, nil)
|
||||
cfg, err := Parse(gs, bc, trustedServerConfig(t))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse() returned unexpected error: %v", err)
|
||||
}
|
||||
defer cfg.Close()
|
||||
|
||||
// The backend certificate's SANs cover x.test.example.com.
|
||||
cc, err := cfg.Dial(grpc.WithAuthority("x.test.example.com"))
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() returned unexpected error: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if _, err := testgrpc.NewTestServiceClient(cc).EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestConfigEqual(t *testing.T) {
|
||||
insecurePlugin := &anypb.Any{TypeUrl: insecureCredsTypeURL}
|
||||
protoInsecure := xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil)
|
||||
jsonInsecure := xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *Config
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure, Timeout: 1, InitialMetadata: metadata.Pairs("k", "v")},
|
||||
b: &Config{TargetURI: target, ChannelCredentials: xdscreds.NewChannelCreds(nil, protoIdentity(insecurePlugin), nil), Timeout: 1, InitialMetadata: metadata.Pairs("k", "v")},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different_timeouts",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure, Timeout: 1},
|
||||
b: &Config{TargetURI: target, ChannelCredentials: protoInsecure, Timeout: 2},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different_initial_metadata",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure},
|
||||
b: &Config{TargetURI: target, ChannelCredentials: protoInsecure, InitialMetadata: metadata.Pairs("k", "v")},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different_targets",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure},
|
||||
b: &Config{TargetURI: "dns:///other:443", ChannelCredentials: protoInsecure},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different_identity_flavors",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure},
|
||||
b: &Config{TargetURI: target, ChannelCredentials: jsonInsecure},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different_call_creds",
|
||||
a: &Config{TargetURI: target, ChannelCredentials: protoInsecure},
|
||||
b: &Config{TargetURI: target, ChannelCredentials: protoInsecure, CallCredentials: []*xdscreds.CallCreds{
|
||||
xdscreds.NewCallCreds(nil, xdscreds.Identity{Type: "access_token"}, nil),
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := test.a.Equal(test.b); got != test.want {
|
||||
t.Errorf("Equal() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user