github.com/kjdelisle/consul@v1.4.5/agent/xds/server_test.go (about)

     1  package xds
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"text/template"
    15  	"time"
    16  
    17  	envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2"
    18  	"github.com/gogo/protobuf/jsonpb"
    19  	"github.com/stretchr/testify/require"
    20  	"google.golang.org/grpc/codes"
    21  	"google.golang.org/grpc/metadata"
    22  	"google.golang.org/grpc/status"
    23  
    24  	"github.com/hashicorp/consul/acl"
    25  	"github.com/hashicorp/consul/agent/cache"
    26  	"github.com/hashicorp/consul/agent/proxycfg"
    27  	"github.com/hashicorp/consul/agent/structs"
    28  )
    29  
    30  // testManager is a mock of proxycfg.Manager that's simpler to control for
    31  // testing. It also implements ConnectAuthz to allow control over authorization.
    32  type testManager struct {
    33  	sync.Mutex
    34  	chans   map[string]chan *proxycfg.ConfigSnapshot
    35  	cancels chan string
    36  	authz   map[string]connectAuthzResult
    37  }
    38  
    39  type connectAuthzResult struct {
    40  	authz  bool
    41  	reason string
    42  	m      *cache.ResultMeta
    43  	err    error
    44  }
    45  
    46  func newTestManager(t *testing.T) *testManager {
    47  	return &testManager{
    48  		chans:   map[string]chan *proxycfg.ConfigSnapshot{},
    49  		cancels: make(chan string, 10),
    50  		authz:   make(map[string]connectAuthzResult),
    51  	}
    52  }
    53  
    54  // RegisterProxy simulates a proxy registration
    55  func (m *testManager) RegisterProxy(t *testing.T, proxyID string) {
    56  	m.Lock()
    57  	defer m.Unlock()
    58  	m.chans[proxyID] = make(chan *proxycfg.ConfigSnapshot, 1)
    59  }
    60  
    61  // Deliver simulates a proxy registration
    62  func (m *testManager) DeliverConfig(t *testing.T, proxyID string, cfg *proxycfg.ConfigSnapshot) {
    63  	t.Helper()
    64  	m.Lock()
    65  	defer m.Unlock()
    66  	select {
    67  	case m.chans[proxyID] <- cfg:
    68  	case <-time.After(10 * time.Millisecond):
    69  		t.Fatalf("took too long to deliver config")
    70  	}
    71  }
    72  
    73  // Watch implements ConfigManager
    74  func (m *testManager) Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) {
    75  	m.Lock()
    76  	defer m.Unlock()
    77  	// ch might be nil but then it will just block forever
    78  	return m.chans[proxyID], func() {
    79  		m.cancels <- proxyID
    80  	}
    81  }
    82  
    83  // AssertWatchCancelled checks that the most recent call to a Watch cancel func
    84  // was from the specified proxyID and that one is made in a short time. This
    85  // probably won't work if you are running multiple Watches in parallel on
    86  // multiple proxyIDS due to timing/ordering issues but I don't think we need to
    87  // do that.
    88  func (m *testManager) AssertWatchCancelled(t *testing.T, proxyID string) {
    89  	t.Helper()
    90  	select {
    91  	case got := <-m.cancels:
    92  		require.Equal(t, proxyID, got)
    93  	case <-time.After(50 * time.Millisecond):
    94  		t.Fatalf("timed out waiting for Watch cancel for %s", proxyID)
    95  	}
    96  }
    97  
    98  // ConnectAuthorize implements ConnectAuthz
    99  func (m *testManager) ConnectAuthorize(token string, req *structs.ConnectAuthorizeRequest) (authz bool, reason string, meta *cache.ResultMeta, err error) {
   100  	m.Lock()
   101  	defer m.Unlock()
   102  	if res, ok := m.authz[token]; ok {
   103  		return res.authz, res.reason, res.m, res.err
   104  	}
   105  	// Default allow but with reason that won't match by accident in a test case
   106  	return true, "OK: allowed by default test implementation", nil, nil
   107  }
   108  
   109  func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) {
   110  	logger := log.New(os.Stderr, "", log.LstdFlags)
   111  	mgr := newTestManager(t)
   112  	aclResolve := func(id string) (acl.Authorizer, error) {
   113  		// Allow all
   114  		return acl.RootAuthorizer("manage"), nil
   115  	}
   116  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", "")
   117  	defer envoy.Close()
   118  
   119  	s := Server{
   120  		Logger:       logger,
   121  		CfgMgr:       mgr,
   122  		Authz:        mgr,
   123  		ResolveToken: aclResolve,
   124  	}
   125  	s.Initialize()
   126  
   127  	go func() {
   128  		err := s.StreamAggregatedResources(envoy.stream)
   129  		require.NoError(t, err)
   130  	}()
   131  
   132  	// Register the proxy to create state needed to Watch() on
   133  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   134  
   135  	// Send initial cluster discover
   136  	envoy.SendReq(t, ClusterType, 0, 0)
   137  
   138  	// Check no response sent yet
   139  	assertChanBlocked(t, envoy.stream.sendCh)
   140  
   141  	// Deliver a new snapshot
   142  	snap := proxycfg.TestConfigSnapshot(t)
   143  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   144  
   145  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 1, 1))
   146  
   147  	// Envoy then tries to discover endpoints for those clusters. Technically it
   148  	// includes the cluster names in the ResourceNames field but we ignore that
   149  	// completely for now so not bothering to simulate that.
   150  	envoy.SendReq(t, EndpointType, 0, 0)
   151  
   152  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   153  	// of the version we sent)
   154  	envoy.SendReq(t, ClusterType, 1, 1)
   155  
   156  	// We should get a response immediately since the config is already present in
   157  	// the server for endpoints. Note that this should not be racy if the server
   158  	// is behaving well since the Cluster send above should be blocked until we
   159  	// deliver a new config version.
   160  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 1, 2))
   161  
   162  	// And no other response yet
   163  	assertChanBlocked(t, envoy.stream.sendCh)
   164  
   165  	// Envoy now sends listener request along with next endpoint one
   166  	envoy.SendReq(t, ListenerType, 0, 0)
   167  	envoy.SendReq(t, EndpointType, 1, 2)
   168  
   169  	// And should get a response immediately.
   170  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 1, 3))
   171  
   172  	// Now send Route request along with next listener one
   173  	envoy.SendReq(t, RouteType, 0, 0)
   174  	envoy.SendReq(t, ListenerType, 1, 3)
   175  
   176  	// We don't serve routes yet so this should block with no response
   177  	assertChanBlocked(t, envoy.stream.sendCh)
   178  
   179  	// WOOP! Envoy now has full connect config. Lets verify that if we update it,
   180  	// all the responses get resent with the new version. We don't actually want
   181  	// to change everything because that's tedious - our implementation will
   182  	// actually resend all blocked types on the new "version" anyway since it
   183  	// doesn't know _what_ changed. We could do something trivial but let's
   184  	// simulate a leaf cert expiring and being rotated.
   185  	snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0])
   186  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   187  
   188  	// All 3 response that have something to return should return with new version
   189  	// note that the ordering is not deterministic in general. Trying to make this
   190  	// test order-agnostic though is a massive pain since we are comparing
   191  	// non-identical JSON strings (so can simply sort by anything) and because we
   192  	// don't know the order the nonces will be assigned. For now we rely and
   193  	// require our implementation to always deliver updates in a specific order
   194  	// which is reasonable anyway to ensure consistency of the config Envoy sees.
   195  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 2, 4))
   196  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 2, 5))
   197  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 2, 6))
   198  
   199  	// Let's pretend that Envoy doesn't like that new listener config. It will ACK
   200  	// all the others (same version) but NACK the listener. This is the most
   201  	// subtle part of xDS and the server implementation so I'll elaborate. A full
   202  	// description of the protocol can be found at
   203  	// https://github.com/envoyproxy/data-plane-api/blob/master/XDS_PROTOCOL.md.
   204  	// Envoy delays making a followup request for a type until after it has
   205  	// processed and applied the last response. The next request then will include
   206  	// the nonce in the last response which acknowledges _receiving_ and handling
   207  	// that response. It also includes the currently applied version. If all is
   208  	// good and it successfully applies the config, then the version in the next
   209  	// response will be the same version just sent. This is considered to be an
   210  	// ACK of that version for that type. If envoy fails to apply the config for
   211  	// some reason, it will still acknowledge that it received it (still return
   212  	// the responses nonce), but will show the previous version it's still using.
   213  	// This is considered a NACK. It's important that the server pay attention to
   214  	// the _nonce_ and not the version when deciding what to send otherwise a bad
   215  	// version that can't be applied in Envoy will cause a busy loop.
   216  	//
   217  	// In this case we are simulating that Envoy failed to apply the Listener
   218  	// response but did apply the other types so all get the new nonces, but
   219  	// listener stays on v1.
   220  	envoy.SendReq(t, ClusterType, 2, 4)
   221  	envoy.SendReq(t, EndpointType, 2, 5)
   222  	envoy.SendReq(t, ListenerType, 1, 6) // v1 is a NACK
   223  
   224  	// Even though we nacked, we should still NOT get then v2 listeners
   225  	// redelivered since nothing has changed.
   226  	assertChanBlocked(t, envoy.stream.sendCh)
   227  
   228  	// Change config again and make sure it's delivered to everyone!
   229  	snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0])
   230  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   231  
   232  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 3, 7))
   233  	assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 3, 8))
   234  	assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 3, 9))
   235  }
   236  
   237  func expectListenerJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string {
   238  	tokenVal := ""
   239  	if token != "" {
   240  		tokenVal = fmt.Sprintf(",\n"+`"value": "%s"`, token)
   241  	}
   242  	return map[string]string{
   243  		"public_listener": `{
   244  													"@type": "type.googleapis.com/envoy.api.v2.Listener",
   245  													"name": "public_listener:0.0.0.0:9999",
   246  													"address": {
   247  														"socketAddress": {
   248  															"address": "0.0.0.0",
   249  															"portValue": 9999
   250  														}
   251  													},
   252  													"filterChains": [
   253  														{
   254  															"tlsContext": ` + expectedPublicTLSContextJSON(t, snap) + `,
   255  															"filters": [
   256  																{
   257  																	"name": "envoy.ext_authz",
   258  																	"config": {
   259  																			"grpc_service": {
   260  																					"envoy_grpc": {
   261  																						"cluster_name": "local_agent"
   262  																					},
   263  																					"initial_metadata": [
   264  																						{
   265  																							"key": "x-consul-token"
   266  																							` + tokenVal + `
   267  																						}
   268  																					]
   269  																				},
   270  																			"stat_prefix": "connect_authz"
   271  																		}
   272  																},
   273  																{
   274  																	"name": "envoy.tcp_proxy",
   275  																	"config": {
   276  																			"cluster": "local_app",
   277  																			"stat_prefix": "public_listener"
   278  																		}
   279  																}
   280  															]
   281  														}
   282  													]
   283  												}`,
   284  		"service:db": `{
   285  										"@type": "type.googleapis.com/envoy.api.v2.Listener",
   286  										"name": "service:db:127.0.0.1:9191",
   287  										"address": {
   288  											"socketAddress": {
   289  												"address": "127.0.0.1",
   290  												"portValue": 9191
   291  											}
   292  										},
   293  										"filterChains": [
   294  											{
   295  												"filters": [
   296  													{
   297  														"name": "envoy.tcp_proxy",
   298  														"config": {
   299  																"cluster": "service:db",
   300  																"stat_prefix": "service:db"
   301  															}
   302  													}
   303  												]
   304  											}
   305  										]
   306  									}`,
   307  		"prepared_query:geo-cache": `{
   308  																	"@type": "type.googleapis.com/envoy.api.v2.Listener",
   309  																	"name": "prepared_query:geo-cache:127.10.10.10:8181",
   310  																	"address": {
   311  																		"socketAddress": {
   312  																			"address": "127.10.10.10",
   313  																			"portValue": 8181
   314  																		}
   315  																	},
   316  																	"filterChains": [
   317  																		{
   318  																			"filters": [
   319  																				{
   320  																					"name": "envoy.tcp_proxy",
   321  																					"config": {
   322  																							"cluster": "prepared_query:geo-cache",
   323  																							"stat_prefix": "prepared_query:geo-cache"
   324  																						}
   325  																				}
   326  																			]
   327  																		}
   328  																	]
   329  																}`,
   330  	}
   331  }
   332  
   333  func expectListenerJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string {
   334  	resJSON := ""
   335  	// Sort resources into specific order because that matters in JSONEq
   336  	// comparison later.
   337  	keyOrder := []string{"public_listener"}
   338  	for _, u := range snap.Proxy.Upstreams {
   339  		keyOrder = append(keyOrder, u.Identifier())
   340  	}
   341  	for _, k := range keyOrder {
   342  		j, ok := resourcesJSON[k]
   343  		if !ok {
   344  			continue
   345  		}
   346  		if resJSON != "" {
   347  			resJSON += ",\n"
   348  		}
   349  		resJSON += j
   350  	}
   351  	return `{
   352  		"versionInfo": "` + hexString(v) + `",
   353  		"resources": [` + resJSON + `],
   354  		"typeUrl": "type.googleapis.com/envoy.api.v2.Listener",
   355  		"nonce": "` + hexString(n) + `"
   356  		}`
   357  }
   358  
   359  func expectListenerJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   360  	return expectListenerJSONFromResources(t, snap, token, v, n,
   361  		expectListenerJSONResources(t, snap, token, v, n))
   362  }
   363  
   364  func expectClustersJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string {
   365  	return map[string]string{
   366  		"local_app": `
   367  			{
   368  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   369  				"name": "local_app",
   370  				"connectTimeout": "5s",
   371  				"hosts": [
   372  					{
   373  						"socketAddress": {
   374  							"address": "127.0.0.1",
   375  							"portValue": 8080
   376  						}
   377  					}
   378  				]
   379  			}`,
   380  		"service:db": `
   381  			{
   382  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   383  				"name": "service:db",
   384  				"type": "EDS",
   385  				"edsClusterConfig": {
   386  					"edsConfig": {
   387  						"ads": {
   388  
   389  						}
   390  					}
   391  				},
   392  				"connectTimeout": "5s",
   393  				"tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + `
   394  			}`,
   395  		"prepared_query:geo-cache": `
   396  			{
   397  				"@type": "type.googleapis.com/envoy.api.v2.Cluster",
   398  				"name": "prepared_query:geo-cache",
   399  				"type": "EDS",
   400  				"edsClusterConfig": {
   401  					"edsConfig": {
   402  						"ads": {
   403  
   404  						}
   405  					}
   406  				},
   407  				"connectTimeout": "5s",
   408  				"tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + `
   409  			}`,
   410  	}
   411  }
   412  
   413  func expectClustersJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string {
   414  	resJSON := ""
   415  
   416  	// Sort resources into specific order because that matters in JSONEq
   417  	// comparison later.
   418  	keyOrder := []string{"local_app"}
   419  	for _, u := range snap.Proxy.Upstreams {
   420  		keyOrder = append(keyOrder, u.Identifier())
   421  	}
   422  	for _, k := range keyOrder {
   423  		j, ok := resourcesJSON[k]
   424  		if !ok {
   425  			continue
   426  		}
   427  		if resJSON != "" {
   428  			resJSON += ",\n"
   429  		}
   430  		resJSON += j
   431  	}
   432  
   433  	return `{
   434  		"versionInfo": "` + hexString(v) + `",
   435  		"resources": [` + resJSON + `],
   436  		"typeUrl": "type.googleapis.com/envoy.api.v2.Cluster",
   437  		"nonce": "` + hexString(n) + `"
   438  		}`
   439  }
   440  
   441  func expectClustersJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   442  	return expectClustersJSONFromResources(t, snap, token, v, n,
   443  		expectClustersJSONResources(t, snap, token, v, n))
   444  }
   445  
   446  func expectEndpointsJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string {
   447  	return `{
   448  		"versionInfo": "` + hexString(v) + `",
   449  		"resources": [
   450  			{
   451  				"@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
   452  				"clusterName": "service:db",
   453  				"endpoints": [
   454  					{
   455  						"lbEndpoints": [
   456  							{
   457  								"endpoint": {
   458  									"address": {
   459  										"socketAddress": {
   460  											"address": "10.10.1.1",
   461  											"portValue": 0
   462  										}
   463  									}
   464  								}
   465  							},
   466  							{
   467  								"endpoint": {
   468  									"address": {
   469  										"socketAddress": {
   470  											"address": "10.10.1.2",
   471  											"portValue": 0
   472  										}
   473  									}
   474  								}
   475  							}
   476  						]
   477  					}
   478  				]
   479  			}
   480  		],
   481  		"typeUrl": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment",
   482  		"nonce": "` + hexString(n) + `"
   483  	}`
   484  }
   485  
   486  func expectedUpstreamTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string {
   487  	return expectedTLSContextJSON(t, snap, false)
   488  }
   489  
   490  func expectedPublicTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string {
   491  	return expectedTLSContextJSON(t, snap, true)
   492  }
   493  
   494  func expectedTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, requireClientCert bool) string {
   495  	// Assume just one root for now, can get fancier later if needed.
   496  	caPEM := snap.Roots.Roots[0].RootCert
   497  	reqClient := ""
   498  	if requireClientCert {
   499  		reqClient = `,
   500  		"requireClientCertificate": true`
   501  	}
   502  	return `{
   503  		"commonTlsContext": {
   504  			"tlsParams": {},
   505  			"tlsCertificates": [
   506  				{
   507  					"certificateChain": {
   508  						"inlineString": "` + strings.Replace(snap.Leaf.CertPEM, "\n", "\\n", -1) + `"
   509  					},
   510  					"privateKey": {
   511  						"inlineString": "` + strings.Replace(snap.Leaf.PrivateKeyPEM, "\n", "\\n", -1) + `"
   512  					}
   513  				}
   514  			],
   515  			"validationContext": {
   516  				"trustedCa": {
   517  					"inlineString": "` + strings.Replace(caPEM, "\n", "\\n", -1) + `"
   518  				}
   519  			}
   520  		}
   521  		` + reqClient + `
   522  	}`
   523  }
   524  
   525  func assertChanBlocked(t *testing.T, ch chan *envoy.DiscoveryResponse) {
   526  	t.Helper()
   527  	select {
   528  	case r := <-ch:
   529  		t.Fatalf("chan should block but received: %v", r)
   530  	case <-time.After(10 * time.Millisecond):
   531  		return
   532  	}
   533  }
   534  
   535  func assertResponseSent(t *testing.T, ch chan *envoy.DiscoveryResponse, wantJSON string) {
   536  	t.Helper()
   537  	select {
   538  	case r := <-ch:
   539  		assertResponse(t, r, wantJSON)
   540  	case <-time.After(50 * time.Millisecond):
   541  		t.Fatalf("no response received after 50ms")
   542  	}
   543  }
   544  
   545  // assertResponse is a helper to test a envoy.DiscoveryResponse matches the
   546  // JSON representation we expect. We use JSON because the responses use protobuf
   547  // Any type which includes binary protobuf encoding and would make creating
   548  // expected structs require the same code that is under test!
   549  func assertResponse(t *testing.T, r *envoy.DiscoveryResponse, wantJSON string) {
   550  	t.Helper()
   551  	m := jsonpb.Marshaler{
   552  		Indent: "  ",
   553  	}
   554  	gotJSON, err := m.MarshalToString(r)
   555  	require.NoError(t, err)
   556  	require.JSONEqf(t, wantJSON, gotJSON, "got:\n%s", gotJSON)
   557  }
   558  
   559  func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) {
   560  
   561  	tests := []struct {
   562  		name        string
   563  		defaultDeny bool
   564  		acl         string
   565  		token       string
   566  		wantDenied  bool
   567  	}{
   568  		// Note that although we've stubbed actual ACL checks in the testManager
   569  		// ConnectAuthorize mock, by asserting against specific reason strings here
   570  		// even in the happy case which can't match the default one returned by the
   571  		// mock we are implicitly validating that the implementation used the
   572  		// correct token from the context.
   573  		{
   574  			name:        "no ACLs configured",
   575  			defaultDeny: false,
   576  			wantDenied:  false,
   577  		},
   578  		{
   579  			name:        "default deny, no token",
   580  			defaultDeny: true,
   581  			wantDenied:  true,
   582  		},
   583  		{
   584  			name:        "default deny, service:write token",
   585  			defaultDeny: true,
   586  			acl:         `service "web" { policy = "write" }`,
   587  			token:       "service-write-on-web",
   588  			wantDenied:  false,
   589  		},
   590  		{
   591  			name:        "default deny, service:read token",
   592  			defaultDeny: true,
   593  			acl:         `service "web" { policy = "read" }`,
   594  			token:       "service-write-on-web",
   595  			wantDenied:  true,
   596  		},
   597  		{
   598  			name:        "default deny, service:write token on different service",
   599  			defaultDeny: true,
   600  			acl:         `service "not-web" { policy = "write" }`,
   601  			token:       "service-write-on-not-web",
   602  			wantDenied:  true,
   603  		},
   604  	}
   605  
   606  	for _, tt := range tests {
   607  		t.Run(tt.name, func(t *testing.T) {
   608  			logger := log.New(os.Stderr, "", log.LstdFlags)
   609  			mgr := newTestManager(t)
   610  			aclResolve := func(id string) (acl.Authorizer, error) {
   611  				if !tt.defaultDeny {
   612  					// Allow all
   613  					return acl.RootAuthorizer("allow"), nil
   614  				}
   615  				if tt.acl == "" {
   616  					// No token and defaultDeny is denied
   617  					return acl.RootAuthorizer("deny"), nil
   618  				}
   619  				// Ensure the correct token was passed
   620  				require.Equal(t, tt.token, id)
   621  				// Parse the ACL and enforce it
   622  				policy, err := acl.NewPolicyFromSource("", 0, tt.acl, acl.SyntaxLegacy, nil)
   623  				require.NoError(t, err)
   624  				return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   625  			}
   626  			envoy := NewTestEnvoy(t, "web-sidecar-proxy", tt.token)
   627  			defer envoy.Close()
   628  
   629  			s := Server{
   630  				Logger:       logger,
   631  				CfgMgr:       mgr,
   632  				Authz:        mgr,
   633  				ResolveToken: aclResolve,
   634  			}
   635  			s.Initialize()
   636  
   637  			errCh := make(chan error, 1)
   638  			go func() {
   639  				errCh <- s.StreamAggregatedResources(envoy.stream)
   640  			}()
   641  
   642  			// Register the proxy to create state needed to Watch() on
   643  			mgr.RegisterProxy(t, "web-sidecar-proxy")
   644  
   645  			// Deliver a new snapshot
   646  			snap := proxycfg.TestConfigSnapshot(t)
   647  			mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   648  
   649  			// Send initial listener discover, in real life Envoy always sends cluster
   650  			// first but it doesn't really matter and listener has a response that
   651  			// includes the token in the ext authz filter so lets us test more stuff.
   652  			envoy.SendReq(t, ListenerType, 0, 0)
   653  
   654  			if !tt.wantDenied {
   655  				assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, tt.token, 1, 1))
   656  				// Close the client stream since all is well. We _don't_ do this in the
   657  				// expected error case because we want to verify the error closes the
   658  				// stream from server side.
   659  				envoy.Close()
   660  			}
   661  
   662  			select {
   663  			case err := <-errCh:
   664  				if tt.wantDenied {
   665  					require.Error(t, err)
   666  					require.Contains(t, err.Error(), "permission denied")
   667  					mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   668  				} else {
   669  					require.NoError(t, err)
   670  				}
   671  			case <-time.After(50 * time.Millisecond):
   672  				t.Fatalf("timed out waiting for handler to finish")
   673  			}
   674  		})
   675  	}
   676  }
   677  
   678  func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuringDiscoveryRequest(t *testing.T) {
   679  	aclRules := `service "web" { policy = "write" }`
   680  	token := "service-write-on-web"
   681  
   682  	policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil)
   683  	require.NoError(t, err)
   684  
   685  	var validToken atomic.Value
   686  	validToken.Store(token)
   687  
   688  	logger := log.New(os.Stderr, "", log.LstdFlags)
   689  	mgr := newTestManager(t)
   690  	aclResolve := func(id string) (acl.Authorizer, error) {
   691  		if token := validToken.Load(); token == nil || id != token.(string) {
   692  			return nil, acl.ErrNotFound
   693  		}
   694  
   695  		return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   696  	}
   697  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   698  	defer envoy.Close()
   699  
   700  	s := Server{
   701  		Logger:             logger,
   702  		CfgMgr:             mgr,
   703  		Authz:              mgr,
   704  		ResolveToken:       aclResolve,
   705  		AuthCheckFrequency: 1 * time.Hour, // make sure this doesn't kick in
   706  	}
   707  	s.Initialize()
   708  
   709  	errCh := make(chan error, 1)
   710  	go func() {
   711  		errCh <- s.StreamAggregatedResources(envoy.stream)
   712  	}()
   713  
   714  	getError := func() (gotErr error, ok bool) {
   715  		select {
   716  		case err := <-errCh:
   717  			return err, true
   718  		default:
   719  			return nil, false
   720  		}
   721  	}
   722  
   723  	// Register the proxy to create state needed to Watch() on
   724  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   725  
   726  	// Send initial cluster discover (OK)
   727  	envoy.SendReq(t, ClusterType, 0, 0)
   728  	{
   729  		err, ok := getError()
   730  		require.NoError(t, err)
   731  		require.False(t, ok)
   732  	}
   733  
   734  	// Check no response sent yet
   735  	assertChanBlocked(t, envoy.stream.sendCh)
   736  	{
   737  		err, ok := getError()
   738  		require.NoError(t, err)
   739  		require.False(t, ok)
   740  	}
   741  
   742  	// Deliver a new snapshot
   743  	snap := proxycfg.TestConfigSnapshot(t)
   744  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   745  
   746  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1))
   747  
   748  	// Now nuke the ACL token.
   749  	validToken.Store("")
   750  
   751  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   752  	// of the version we sent)
   753  	envoy.SendReq(t, ClusterType, 1, 1)
   754  
   755  	select {
   756  	case err := <-errCh:
   757  		require.Error(t, err)
   758  		gerr, ok := status.FromError(err)
   759  		require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err)
   760  		require.Equal(t, codes.Unauthenticated, gerr.Code())
   761  		require.Equal(t, "unauthenticated: ACL not found", gerr.Message())
   762  
   763  		mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   764  	case <-time.After(50 * time.Millisecond):
   765  		t.Fatalf("timed out waiting for handler to finish")
   766  	}
   767  }
   768  
   769  func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBackground(t *testing.T) {
   770  	aclRules := `service "web" { policy = "write" }`
   771  	token := "service-write-on-web"
   772  
   773  	policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil)
   774  	require.NoError(t, err)
   775  
   776  	var validToken atomic.Value
   777  	validToken.Store(token)
   778  
   779  	logger := log.New(os.Stderr, "", log.LstdFlags)
   780  	mgr := newTestManager(t)
   781  	aclResolve := func(id string) (acl.Authorizer, error) {
   782  		if token := validToken.Load(); token == nil || id != token.(string) {
   783  			return nil, acl.ErrNotFound
   784  		}
   785  
   786  		return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil)
   787  	}
   788  	envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   789  	defer envoy.Close()
   790  
   791  	s := Server{
   792  		Logger:             logger,
   793  		CfgMgr:             mgr,
   794  		Authz:              mgr,
   795  		ResolveToken:       aclResolve,
   796  		AuthCheckFrequency: 100 * time.Millisecond, // Make this short.
   797  	}
   798  	s.Initialize()
   799  
   800  	errCh := make(chan error, 1)
   801  	go func() {
   802  		errCh <- s.StreamAggregatedResources(envoy.stream)
   803  	}()
   804  
   805  	getError := func() (gotErr error, ok bool) {
   806  		select {
   807  		case err := <-errCh:
   808  			return err, true
   809  		default:
   810  			return nil, false
   811  		}
   812  	}
   813  
   814  	// Register the proxy to create state needed to Watch() on
   815  	mgr.RegisterProxy(t, "web-sidecar-proxy")
   816  
   817  	// Send initial cluster discover (OK)
   818  	envoy.SendReq(t, ClusterType, 0, 0)
   819  	{
   820  		err, ok := getError()
   821  		require.NoError(t, err)
   822  		require.False(t, ok)
   823  	}
   824  
   825  	// Check no response sent yet
   826  	assertChanBlocked(t, envoy.stream.sendCh)
   827  	{
   828  		err, ok := getError()
   829  		require.NoError(t, err)
   830  		require.False(t, ok)
   831  	}
   832  
   833  	// Deliver a new snapshot
   834  	snap := proxycfg.TestConfigSnapshot(t)
   835  	mgr.DeliverConfig(t, "web-sidecar-proxy", snap)
   836  
   837  	assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1))
   838  
   839  	// It also (in parallel) issues the next cluster request (which acts as an ACK
   840  	// of the version we sent)
   841  	envoy.SendReq(t, ClusterType, 1, 1)
   842  
   843  	// Check no response sent yet
   844  	assertChanBlocked(t, envoy.stream.sendCh)
   845  	{
   846  		err, ok := getError()
   847  		require.NoError(t, err)
   848  		require.False(t, ok)
   849  	}
   850  
   851  	// Now nuke the ACL token while there's no activity.
   852  	validToken.Store("")
   853  
   854  	select {
   855  	case err := <-errCh:
   856  		require.Error(t, err)
   857  		gerr, ok := status.FromError(err)
   858  		require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err)
   859  		require.Equal(t, codes.Unauthenticated, gerr.Code())
   860  		require.Equal(t, "unauthenticated: ACL not found", gerr.Message())
   861  
   862  		mgr.AssertWatchCancelled(t, "web-sidecar-proxy")
   863  	case <-time.After(200 * time.Millisecond):
   864  		t.Fatalf("timed out waiting for handler to finish")
   865  	}
   866  }
   867  
   868  // This tests the ext_authz service method that implements connect authz.
   869  func TestServer_Check(t *testing.T) {
   870  
   871  	tests := []struct {
   872  		name            string
   873  		source          string
   874  		dest            string
   875  		sourcePrincipal string
   876  		destPrincipal   string
   877  		authzResult     connectAuthzResult
   878  		wantErr         bool
   879  		wantErrCode     codes.Code
   880  		wantDenied      bool
   881  		wantReason      string
   882  	}{
   883  		{
   884  			name:        "auth allowed",
   885  			source:      "web",
   886  			dest:        "db",
   887  			authzResult: connectAuthzResult{true, "default allow", nil, nil},
   888  			wantDenied:  false,
   889  			wantReason:  "default allow",
   890  		},
   891  		{
   892  			name:        "auth denied",
   893  			source:      "web",
   894  			dest:        "db",
   895  			authzResult: connectAuthzResult{false, "default deny", nil, nil},
   896  			wantDenied:  true,
   897  			wantReason:  "default deny",
   898  		},
   899  		{
   900  			name:            "no source",
   901  			sourcePrincipal: "",
   902  			dest:            "db",
   903  			// Should never make it to authz call.
   904  			wantErr:     true,
   905  			wantErrCode: codes.InvalidArgument,
   906  		},
   907  		{
   908  			name:   "no dest",
   909  			source: "web",
   910  			dest:   "",
   911  			// Should never make it to authz call.
   912  			wantErr:     true,
   913  			wantErrCode: codes.InvalidArgument,
   914  		},
   915  		{
   916  			name:          "dest invalid format",
   917  			source:        "web",
   918  			destPrincipal: "not-a-spiffe-id",
   919  			// Should never make it to authz call.
   920  			wantDenied: true,
   921  			wantReason: "Destination Principal is not a valid Connect identity",
   922  		},
   923  		{
   924  			name:          "dest not a service URI",
   925  			source:        "web",
   926  			destPrincipal: "spiffe://trust-domain.consul",
   927  			// Should never make it to authz call.
   928  			wantDenied: true,
   929  			wantReason: "Destination Principal is not a valid Service identity",
   930  		},
   931  		{
   932  			name:        "ACL not got permission for authz call",
   933  			source:      "web",
   934  			dest:        "db",
   935  			authzResult: connectAuthzResult{false, "", nil, acl.ErrPermissionDenied},
   936  			wantErr:     true,
   937  			wantErrCode: codes.PermissionDenied,
   938  		},
   939  		{
   940  			name:        "Random error running authz",
   941  			source:      "web",
   942  			dest:        "db",
   943  			authzResult: connectAuthzResult{false, "", nil, errors.New("gremlin attack")},
   944  			wantErr:     true,
   945  			wantErrCode: codes.Internal,
   946  		},
   947  	}
   948  
   949  	for _, tt := range tests {
   950  		t.Run(tt.name, func(t *testing.T) {
   951  			token := "my-real-acl-token"
   952  			logger := log.New(os.Stderr, "", log.LstdFlags)
   953  			mgr := newTestManager(t)
   954  
   955  			// Setup expected auth result against that token no lock as no other
   956  			// goroutine is touching this yet.
   957  			mgr.authz[token] = tt.authzResult
   958  
   959  			aclResolve := func(id string) (acl.Authorizer, error) {
   960  				return nil, nil
   961  			}
   962  			envoy := NewTestEnvoy(t, "web-sidecar-proxy", token)
   963  			defer envoy.Close()
   964  
   965  			s := Server{
   966  				Logger:       logger,
   967  				CfgMgr:       mgr,
   968  				Authz:        mgr,
   969  				ResolveToken: aclResolve,
   970  			}
   971  			s.Initialize()
   972  
   973  			// Create a context with the correct token
   974  			ctx := metadata.NewIncomingContext(context.Background(),
   975  				metadata.Pairs("x-consul-token", token))
   976  
   977  			r := TestCheckRequest(t, tt.source, tt.dest)
   978  			// If sourcePrincipal is set override, or if source is also not set
   979  			// explicitly override to empty.
   980  			if tt.sourcePrincipal != "" || tt.source == "" {
   981  				r.Attributes.Source.Principal = tt.sourcePrincipal
   982  			}
   983  			if tt.destPrincipal != "" || tt.dest == "" {
   984  				r.Attributes.Destination.Principal = tt.destPrincipal
   985  			}
   986  			resp, err := s.Check(ctx, r)
   987  			// Denied is not an error
   988  			if tt.wantErr {
   989  				require.Error(t, err)
   990  				grpcStatus := status.Convert(err)
   991  				require.Equal(t, tt.wantErrCode, grpcStatus.Code())
   992  				require.Nil(t, resp)
   993  				return
   994  			}
   995  			require.NoError(t, err)
   996  			if tt.wantDenied {
   997  				require.Equal(t, int32(codes.PermissionDenied), resp.Status.Code)
   998  			} else {
   999  				require.Equal(t, int32(codes.OK), resp.Status.Code)
  1000  			}
  1001  			require.Contains(t, resp.Status.Message, tt.wantReason)
  1002  		})
  1003  	}
  1004  }
  1005  
  1006  func TestServer_ConfigOverridesListeners(t *testing.T) {
  1007  
  1008  	tests := []struct {
  1009  		name  string
  1010  		setup func(snap *proxycfg.ConfigSnapshot) string
  1011  	}{
  1012  		{
  1013  			name: "sanity check no custom",
  1014  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1015  				// Default snap and expectation
  1016  				return expectListenerJSON(t, snap, "my-token", 1, 1)
  1017  			},
  1018  		},
  1019  		{
  1020  			name: "custom public_listener no type",
  1021  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1022  				snap.Proxy.Config["envoy_public_listener_json"] =
  1023  					customListenerJSON(t, customListenerJSONOptions{
  1024  						Name:        "custom-public-listen",
  1025  						IncludeType: false,
  1026  					})
  1027  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1028  
  1029  				// Replace the public listener with the custom one WITH type since
  1030  				// that's how it comes out the other end, and with TLS and authz
  1031  				// overridden.
  1032  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1033  					Name: "custom-public-listen",
  1034  					// We should add type, TLS and authz
  1035  					IncludeType:   true,
  1036  					OverrideAuthz: true,
  1037  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1038  				})
  1039  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1040  			},
  1041  		},
  1042  		{
  1043  			name: "custom public_listener with type",
  1044  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1045  				snap.Proxy.Config["envoy_public_listener_json"] =
  1046  					customListenerJSON(t, customListenerJSONOptions{
  1047  						Name:        "custom-public-listen",
  1048  						IncludeType: true,
  1049  					})
  1050  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1051  
  1052  				// Replace the public listener with the custom one WITH type since
  1053  				// that's how it comes out the other end, and with TLS and authz
  1054  				// overridden.
  1055  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1056  					Name: "custom-public-listen",
  1057  					// We should add type, TLS and authz
  1058  					IncludeType:   true,
  1059  					OverrideAuthz: true,
  1060  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1061  				})
  1062  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1063  			},
  1064  		},
  1065  		{
  1066  			name: "custom public_listener with TLS should be overridden",
  1067  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1068  				snap.Proxy.Config["envoy_public_listener_json"] =
  1069  					customListenerJSON(t, customListenerJSONOptions{
  1070  						Name:        "custom-public-listen",
  1071  						IncludeType: true,
  1072  						TLSContext:  `{"requireClientCertificate": false}`,
  1073  					})
  1074  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1075  
  1076  				// Replace the public listener with the custom one WITH type since
  1077  				// that's how it comes out the other end, and with TLS and authz
  1078  				// overridden.
  1079  				resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{
  1080  					Name: "custom-public-listen",
  1081  					// We should add type, TLS and authz
  1082  					IncludeType:   true,
  1083  					OverrideAuthz: true,
  1084  					TLSContext:    expectedPublicTLSContextJSON(t, snap),
  1085  				})
  1086  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1087  			},
  1088  		},
  1089  		{
  1090  			name: "custom upstream no type",
  1091  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1092  				snap.Proxy.Upstreams[0].Config["envoy_listener_json"] =
  1093  					customListenerJSON(t, customListenerJSONOptions{
  1094  						Name:        "custom-upstream",
  1095  						IncludeType: false,
  1096  					})
  1097  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1098  
  1099  				// Replace an upstream listener with the custom one WITH type since
  1100  				// that's how it comes out the other end. Note we do override TLS
  1101  				resources["service:db"] =
  1102  					customListenerJSON(t, customListenerJSONOptions{
  1103  						Name: "custom-upstream",
  1104  						// We should add type
  1105  						IncludeType: true,
  1106  					})
  1107  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1108  			},
  1109  		},
  1110  		{
  1111  			name: "custom upstream with type",
  1112  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1113  				snap.Proxy.Upstreams[0].Config["envoy_listener_json"] =
  1114  					customListenerJSON(t, customListenerJSONOptions{
  1115  						Name:        "custom-upstream",
  1116  						IncludeType: true,
  1117  					})
  1118  				resources := expectListenerJSONResources(t, snap, "my-token", 1, 1)
  1119  
  1120  				// Replace an upstream listener with the custom one WITH type since
  1121  				// that's how it comes out the other end.
  1122  				resources["service:db"] =
  1123  					customListenerJSON(t, customListenerJSONOptions{
  1124  						Name: "custom-upstream",
  1125  						// We should add type
  1126  						IncludeType: true,
  1127  					})
  1128  				return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1129  			},
  1130  		},
  1131  	}
  1132  
  1133  	for _, tt := range tests {
  1134  		t.Run(tt.name, func(t *testing.T) {
  1135  			require := require.New(t)
  1136  
  1137  			// Sanity check default with no overrides first
  1138  			snap := proxycfg.TestConfigSnapshot(t)
  1139  			expect := tt.setup(snap)
  1140  
  1141  			listeners, err := listenersFromSnapshot(snap, "my-token")
  1142  			require.NoError(err)
  1143  			r, err := createResponse(ListenerType, "00000001", "00000001", listeners)
  1144  			require.NoError(err)
  1145  
  1146  			assertResponse(t, r, expect)
  1147  		})
  1148  	}
  1149  }
  1150  
  1151  func TestServer_ConfigOverridesClusters(t *testing.T) {
  1152  
  1153  	tests := []struct {
  1154  		name  string
  1155  		setup func(snap *proxycfg.ConfigSnapshot) string
  1156  	}{
  1157  		{
  1158  			name: "sanity check no custom",
  1159  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1160  				// Default snap and expectation
  1161  				return expectClustersJSON(t, snap, "my-token", 1, 1)
  1162  			},
  1163  		},
  1164  		{
  1165  			name: "custom public with no type",
  1166  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1167  				snap.Proxy.Config["envoy_local_cluster_json"] =
  1168  					customAppClusterJSON(t, customClusterJSONOptions{
  1169  						Name:        "mylocal",
  1170  						IncludeType: false,
  1171  					})
  1172  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1173  
  1174  				// Replace an upstream listener with the custom one WITH type since
  1175  				// that's how it comes out the other end.
  1176  				resources["local_app"] =
  1177  					customAppClusterJSON(t, customClusterJSONOptions{
  1178  						Name:        "mylocal",
  1179  						IncludeType: true,
  1180  					})
  1181  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1182  			},
  1183  		},
  1184  		{
  1185  			name: "custom public with type",
  1186  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1187  				snap.Proxy.Config["envoy_local_cluster_json"] =
  1188  					customAppClusterJSON(t, customClusterJSONOptions{
  1189  						Name:        "mylocal",
  1190  						IncludeType: true,
  1191  					})
  1192  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1193  
  1194  				// Replace an upstream listener with the custom one WITH type since
  1195  				// that's how it comes out the other end.
  1196  				resources["local_app"] =
  1197  					customAppClusterJSON(t, customClusterJSONOptions{
  1198  						Name:        "mylocal",
  1199  						IncludeType: true,
  1200  					})
  1201  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1202  			},
  1203  		},
  1204  		{
  1205  			name: "custom upstream with no type",
  1206  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1207  				snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] =
  1208  					customEDSClusterJSON(t, customClusterJSONOptions{
  1209  						Name:        "myservice",
  1210  						IncludeType: false,
  1211  					})
  1212  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1213  
  1214  				// Replace an upstream listener with the custom one WITH type since
  1215  				// that's how it comes out the other end.
  1216  				resources["service:db"] =
  1217  					customEDSClusterJSON(t, customClusterJSONOptions{
  1218  						Name:        "myservice",
  1219  						IncludeType: true,
  1220  						TLSContext:  expectedUpstreamTLSContextJSON(t, snap),
  1221  					})
  1222  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1223  			},
  1224  		},
  1225  		{
  1226  			name: "custom upstream with type",
  1227  			setup: func(snap *proxycfg.ConfigSnapshot) string {
  1228  				snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] =
  1229  					customEDSClusterJSON(t, customClusterJSONOptions{
  1230  						Name:        "myservice",
  1231  						IncludeType: true,
  1232  					})
  1233  				resources := expectClustersJSONResources(t, snap, "my-token", 1, 1)
  1234  
  1235  				// Replace an upstream listener with the custom one WITH type since
  1236  				// that's how it comes out the other end.
  1237  				resources["service:db"] =
  1238  					customEDSClusterJSON(t, customClusterJSONOptions{
  1239  						Name:        "myservice",
  1240  						IncludeType: true,
  1241  						TLSContext:  expectedUpstreamTLSContextJSON(t, snap),
  1242  					})
  1243  				return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources)
  1244  			},
  1245  		},
  1246  	}
  1247  
  1248  	for _, tt := range tests {
  1249  		t.Run(tt.name, func(t *testing.T) {
  1250  			require := require.New(t)
  1251  
  1252  			// Sanity check default with no overrides first
  1253  			snap := proxycfg.TestConfigSnapshot(t)
  1254  			expect := tt.setup(snap)
  1255  
  1256  			clusters, err := clustersFromSnapshot(snap, "my-token")
  1257  			require.NoError(err)
  1258  			r, err := createResponse(ClusterType, "00000001", "00000001", clusters)
  1259  			require.NoError(err)
  1260  
  1261  			fmt.Println(r)
  1262  
  1263  			assertResponse(t, r, expect)
  1264  		})
  1265  	}
  1266  }
  1267  
  1268  type customListenerJSONOptions struct {
  1269  	Name          string
  1270  	IncludeType   bool
  1271  	OverrideAuthz bool
  1272  	TLSContext    string
  1273  }
  1274  
  1275  const customListenerJSONTpl = `{
  1276  	{{ if .IncludeType -}}
  1277  	"@type": "type.googleapis.com/envoy.api.v2.Listener",
  1278  	{{- end }}
  1279  	"name": "{{ .Name }}",
  1280  	"address": {
  1281  		"socketAddress": {
  1282  			"address": "11.11.11.11",
  1283  			"portValue": 11111
  1284  		}
  1285  	},
  1286  	"filterChains": [
  1287  		{
  1288  			{{ if .TLSContext -}}
  1289  			"tlsContext": {{ .TLSContext }},
  1290  			{{- end }}
  1291  			"filters": [
  1292  				{{ if .OverrideAuthz -}}
  1293  				{
  1294  					"name": "envoy.ext_authz",
  1295  					"config": {
  1296  							"grpc_service": {
  1297  										"envoy_grpc": {
  1298  													"cluster_name": "local_agent"
  1299  												},
  1300  										"initial_metadata": [
  1301  													{
  1302  																"key": "x-consul-token",
  1303  																"value": "my-token"
  1304  															}
  1305  												]
  1306  									},
  1307  							"stat_prefix": "connect_authz"
  1308  						}
  1309  				},
  1310  				{{- end }}
  1311  				{
  1312  					"name": "envoy.tcp_proxy",
  1313  					"config": {
  1314  							"cluster": "random-cluster",
  1315  							"stat_prefix": "foo-stats"
  1316  						}
  1317  				}
  1318  			]
  1319  		}
  1320  	]
  1321  }`
  1322  
  1323  var customListenerJSONTemplate = template.Must(template.New("").Parse(customListenerJSONTpl))
  1324  
  1325  func customListenerJSON(t *testing.T, opts customListenerJSONOptions) string {
  1326  	t.Helper()
  1327  	var buf bytes.Buffer
  1328  	err := customListenerJSONTemplate.Execute(&buf, opts)
  1329  	require.NoError(t, err)
  1330  	return buf.String()
  1331  }
  1332  
  1333  type customClusterJSONOptions struct {
  1334  	Name        string
  1335  	IncludeType bool
  1336  	TLSContext  string
  1337  }
  1338  
  1339  var customEDSClusterJSONTpl = `{
  1340  	{{ if .IncludeType -}}
  1341  	"@type": "type.googleapis.com/envoy.api.v2.Cluster",
  1342  	{{- end }}
  1343  	{{ if .TLSContext -}}
  1344  	"tlsContext": {{ .TLSContext }},
  1345  	{{- end }}
  1346  	"name": "{{ .Name }}",
  1347  	"type": "EDS",
  1348  	"edsClusterConfig": {
  1349  		"edsConfig": {
  1350  			"ads": {
  1351  
  1352  			}
  1353  		}
  1354  	},
  1355  	"connectTimeout": "5s"
  1356  }`
  1357  
  1358  var customEDSClusterJSONTemplate = template.Must(template.New("").Parse(customEDSClusterJSONTpl))
  1359  
  1360  func customEDSClusterJSON(t *testing.T, opts customClusterJSONOptions) string {
  1361  	t.Helper()
  1362  	var buf bytes.Buffer
  1363  	err := customEDSClusterJSONTemplate.Execute(&buf, opts)
  1364  	require.NoError(t, err)
  1365  	return buf.String()
  1366  }
  1367  
  1368  var customAppClusterJSONTpl = `{
  1369  	{{ if .IncludeType -}}
  1370  	"@type": "type.googleapis.com/envoy.api.v2.Cluster",
  1371  	{{- end }}
  1372  	{{ if .TLSContext -}}
  1373  	"tlsContext": {{ .TLSContext }},
  1374  	{{- end }}
  1375  	"name": "{{ .Name }}",
  1376  	"connectTimeout": "5s",
  1377  	"hosts": [
  1378  		{
  1379  			"socketAddress": {
  1380  				"address": "127.0.0.1",
  1381  				"portValue": 8080
  1382  			}
  1383  		}
  1384  	]
  1385  }`
  1386  
  1387  var customAppClusterJSONTemplate = template.Must(template.New("").Parse(customAppClusterJSONTpl))
  1388  
  1389  func customAppClusterJSON(t *testing.T, opts customClusterJSONOptions) string {
  1390  	t.Helper()
  1391  	var buf bytes.Buffer
  1392  	err := customAppClusterJSONTemplate.Execute(&buf, opts)
  1393  	require.NoError(t, err)
  1394  	return buf.String()
  1395  }