google.golang.org/grpc@v1.74.2/test/xds/xds_server_rbac_test.go (about)

     1  /*
     2   *
     3   * Copyright 2022 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xds_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"net"
    26  	"strconv"
    27  	"strings"
    28  	"testing"
    29  
    30  	v3xdsxdstypepb "github.com/cncf/xds/go/xds/type/v3"
    31  	v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
    32  	"github.com/google/go-cmp/cmp"
    33  	"google.golang.org/grpc"
    34  	"google.golang.org/grpc/authz/audit"
    35  	"google.golang.org/grpc/codes"
    36  	"google.golang.org/grpc/connectivity"
    37  	"google.golang.org/grpc/credentials/insecure"
    38  	"google.golang.org/grpc/internal"
    39  	"google.golang.org/grpc/internal/testutils"
    40  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    41  	"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
    42  	"google.golang.org/grpc/status"
    43  	"google.golang.org/grpc/xds"
    44  	"google.golang.org/protobuf/types/known/anypb"
    45  	"google.golang.org/protobuf/types/known/structpb"
    46  	"google.golang.org/protobuf/types/known/wrapperspb"
    47  
    48  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    49  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    50  	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    51  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    52  	rpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
    53  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    54  	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
    55  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    56  	testpb "google.golang.org/grpc/interop/grpc_testing"
    57  )
    58  
    59  // TestServerSideXDS_RouteConfiguration is an e2e test which verifies routing
    60  // functionality. The xDS enabled server will be set up with route configuration
    61  // where the route configuration has routes with the correct routing actions
    62  // (NonForwardingAction), and the RPC's matching those routes should proceed as
    63  // normal.
    64  func (s) TestServerSideXDS_RouteConfiguration(t *testing.T) {
    65  	managementServer, nodeID, bootstrapContents, xdsResolver := setup.ManagementServerAndResolver(t)
    66  
    67  	lis, cleanup2 := setupGRPCServer(t, bootstrapContents)
    68  	defer cleanup2()
    69  
    70  	host, port, err := hostPortFromListener(lis)
    71  	if err != nil {
    72  		t.Fatalf("failed to retrieve host and port of server: %v", err)
    73  	}
    74  	const serviceName = "my-service-fallback"
    75  	resources := e2e.DefaultClientResources(e2e.ResourceParams{
    76  		DialTarget: serviceName,
    77  		NodeID:     nodeID,
    78  		Host:       host,
    79  		Port:       port,
    80  		SecLevel:   e2e.SecurityLevelNone,
    81  	})
    82  
    83  	// Create an inbound xDS listener resource with route configuration which
    84  	// selectively will allow RPC's through or not. This will test routing in
    85  	// xds(Unary|Stream)Interceptors.
    86  	vhs := []*v3routepb.VirtualHost{
    87  		// Virtual host that will never be matched to test Virtual Host selection.
    88  		{
    89  			Domains: []string{"this will not match*"},
    90  			Routes: []*v3routepb.Route{
    91  				{
    92  					Match: &v3routepb.RouteMatch{
    93  						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
    94  					},
    95  					Action: &v3routepb.Route_NonForwardingAction{},
    96  				},
    97  			},
    98  		},
    99  		// This Virtual Host will actually get matched to.
   100  		{
   101  			Domains: []string{"*"},
   102  			Routes: []*v3routepb.Route{
   103  				// A routing rule that can be selectively triggered based on properties about incoming RPC.
   104  				{
   105  					Match: &v3routepb.RouteMatch{
   106  						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/EmptyCall"},
   107  						// "Fully-qualified RPC method name with leading slash. Same as :path header".
   108  					},
   109  					// Correct Action, so RPC's that match this route should proceed to interceptor processing.
   110  					Action: &v3routepb.Route_NonForwardingAction{},
   111  				},
   112  				// This routing rule is matched the same way as the one above,
   113  				// except has an incorrect action for the server side. However,
   114  				// since routing chooses the first route which matches an
   115  				// incoming RPC, this should never get invoked (iteration
   116  				// through this route slice is deterministic).
   117  				{
   118  					Match: &v3routepb.RouteMatch{
   119  						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/EmptyCall"},
   120  						// "Fully-qualified RPC method name with leading slash. Same as :path header".
   121  					},
   122  					// Incorrect Action, so RPC's that match this route should get denied.
   123  					Action: &v3routepb.Route_Route{
   124  						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: ""}},
   125  					},
   126  				},
   127  				// Another routing rule that can be selectively triggered based on incoming RPC.
   128  				{
   129  					Match: &v3routepb.RouteMatch{
   130  						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/UnaryCall"},
   131  					},
   132  					// Wrong action (!Non_Forwarding_Action) so RPC's that match this route should get denied.
   133  					Action: &v3routepb.Route_Route{
   134  						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: ""}},
   135  					},
   136  				},
   137  				// Another routing rule that can be selectively triggered based on incoming RPC.
   138  				{
   139  					Match: &v3routepb.RouteMatch{
   140  						PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/grpc.testing.TestService/StreamingInputCall"},
   141  					},
   142  					// Wrong action (!Non_Forwarding_Action) so RPC's that match this route should get denied.
   143  					Action: &v3routepb.Route_Route{
   144  						Route: &v3routepb.RouteAction{ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: ""}},
   145  					},
   146  				},
   147  				// Not matching route, this is be able to get invoked logically (i.e. doesn't have to match the Route configurations above).
   148  			}},
   149  	}
   150  	inboundLis := &v3listenerpb.Listener{
   151  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
   152  		Address: &v3corepb.Address{
   153  			Address: &v3corepb.Address_SocketAddress{
   154  				SocketAddress: &v3corepb.SocketAddress{
   155  					Address: host,
   156  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   157  						PortValue: port,
   158  					},
   159  				},
   160  			},
   161  		},
   162  		FilterChains: []*v3listenerpb.FilterChain{
   163  			{
   164  				Name: "v4-wildcard",
   165  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   166  					PrefixRanges: []*v3corepb.CidrRange{
   167  						{
   168  							AddressPrefix: "0.0.0.0",
   169  							PrefixLen: &wrapperspb.UInt32Value{
   170  								Value: uint32(0),
   171  							},
   172  						},
   173  					},
   174  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   175  					SourcePrefixRanges: []*v3corepb.CidrRange{
   176  						{
   177  							AddressPrefix: "0.0.0.0",
   178  							PrefixLen: &wrapperspb.UInt32Value{
   179  								Value: uint32(0),
   180  							},
   181  						},
   182  					},
   183  				},
   184  				Filters: []*v3listenerpb.Filter{
   185  					{
   186  						Name: "filter-1",
   187  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   188  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   189  								HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
   190  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   191  									RouteConfig: &v3routepb.RouteConfiguration{
   192  										Name:         "routeName",
   193  										VirtualHosts: vhs,
   194  									},
   195  								},
   196  							}),
   197  						},
   198  					},
   199  				},
   200  			},
   201  			{
   202  				Name: "v6-wildcard",
   203  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   204  					PrefixRanges: []*v3corepb.CidrRange{
   205  						{
   206  							AddressPrefix: "::",
   207  							PrefixLen: &wrapperspb.UInt32Value{
   208  								Value: uint32(0),
   209  							},
   210  						},
   211  					},
   212  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   213  					SourcePrefixRanges: []*v3corepb.CidrRange{
   214  						{
   215  							AddressPrefix: "::",
   216  							PrefixLen: &wrapperspb.UInt32Value{
   217  								Value: uint32(0),
   218  							},
   219  						},
   220  					},
   221  				},
   222  				Filters: []*v3listenerpb.Filter{
   223  					{
   224  						Name: "filter-1",
   225  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   226  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   227  								HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
   228  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   229  									RouteConfig: &v3routepb.RouteConfiguration{
   230  										Name:         "routeName",
   231  										VirtualHosts: vhs,
   232  									},
   233  								},
   234  							}),
   235  						},
   236  					},
   237  				},
   238  			},
   239  		},
   240  	}
   241  	resources.Listeners = append(resources.Listeners, inboundLis)
   242  
   243  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   244  	defer cancel()
   245  	// Setup the management server with client and server-side resources.
   246  	if err := managementServer.Update(ctx, resources); err != nil {
   247  		t.Fatal(err)
   248  	}
   249  
   250  	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   251  	if err != nil {
   252  		t.Fatalf("grpc.NewClient() failed: %v", err)
   253  	}
   254  	defer cc.Close()
   255  
   256  	client := testgrpc.NewTestServiceClient(cc)
   257  
   258  	// This Empty Call should match to a route with a correct action
   259  	// (NonForwardingAction). Thus, this RPC should proceed as normal. There is
   260  	// a routing rule that this RPC would match to that has an incorrect action,
   261  	// but the server should only use the first route matched to with the
   262  	// correct action.
   263  	if _, err = client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   264  		t.Fatalf("rpc EmptyCall() failed: %v", err)
   265  	}
   266  
   267  	// This Unary Call should match to a route with an incorrect action. Thus,
   268  	// this RPC should not go through as per A36, and this call should receive
   269  	// an error with codes.Unavailable.
   270  	_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
   271  	if status.Code(err) != codes.Unavailable {
   272  		t.Fatalf("client.UnaryCall() = _, %v, want _, error code %s", err, codes.Unavailable)
   273  	}
   274  	if !strings.Contains(err.Error(), nodeID) {
   275  		t.Fatalf("client.UnaryCall() = %v, want xDS node id %q", err, nodeID)
   276  	}
   277  
   278  	// This Streaming Call should match to a route with an incorrect action.
   279  	// Thus, this RPC should not go through as per A36, and this call should
   280  	// receive an error with codes.Unavailable.
   281  	stream, err := client.StreamingInputCall(ctx)
   282  	if err != nil {
   283  		t.Fatalf("StreamingInputCall(_) = _, %v, want <nil>", err)
   284  	}
   285  	_, err = stream.CloseAndRecv()
   286  	const wantStreamingErr = "the incoming RPC matched to a route that was not of action type non forwarding"
   287  	if status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), wantStreamingErr) {
   288  		t.Fatalf("client.StreamingInputCall() = %v, want error with code %s and message %q", err, codes.Unavailable, wantStreamingErr)
   289  	}
   290  	if !strings.Contains(err.Error(), nodeID) {
   291  		t.Fatalf("client.StreamingInputCall() = %v, want xDS node id %q", err, nodeID)
   292  	}
   293  
   294  	// This Full Duplex should not match to a route, and thus should return an
   295  	// error and not proceed.
   296  	dStream, err := client.FullDuplexCall(ctx)
   297  	if err != nil {
   298  		t.Fatalf("FullDuplexCall(_) = _, %v, want <nil>", err)
   299  	}
   300  	_, err = dStream.Recv()
   301  	const wantFullDuplexErr = "the incoming RPC did not match a configured Route"
   302  	if status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), wantFullDuplexErr) {
   303  		t.Fatalf("client.FullDuplexCall() = %v, want error with code %s and message %q", err, codes.Unavailable, wantFullDuplexErr)
   304  	}
   305  	if !strings.Contains(err.Error(), nodeID) {
   306  		t.Fatalf("client.FullDuplexCall() = %v, want xDS node id %q", err, nodeID)
   307  	}
   308  }
   309  
   310  // serverListenerWithRBACHTTPFilters returns an xds Listener resource with HTTP Filters defined in the HCM, and a route
   311  // configuration that always matches to a route and a VH.
   312  func serverListenerWithRBACHTTPFilters(t *testing.T, host string, port uint32, rbacCfg *rpb.RBAC) *v3listenerpb.Listener {
   313  	// Rather than declare typed config inline, take a HCM proto and append the
   314  	// RBAC Filters to it.
   315  	hcm := &v3httppb.HttpConnectionManager{
   316  		RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   317  			RouteConfig: &v3routepb.RouteConfiguration{
   318  				Name: "routeName",
   319  				VirtualHosts: []*v3routepb.VirtualHost{{
   320  					Domains: []string{"*"},
   321  					Routes: []*v3routepb.Route{{
   322  						Match: &v3routepb.RouteMatch{
   323  							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   324  						},
   325  						Action: &v3routepb.Route_NonForwardingAction{},
   326  					}},
   327  					// This tests override parsing + building when RBAC Filter
   328  					// passed both normal and override config.
   329  					TypedPerFilterConfig: map[string]*anypb.Any{
   330  						"rbac": testutils.MarshalAny(t, &rpb.RBACPerRoute{Rbac: rbacCfg}),
   331  					},
   332  				}}},
   333  		},
   334  	}
   335  	hcm.HttpFilters = nil
   336  	hcm.HttpFilters = append(hcm.HttpFilters, e2e.HTTPFilter("rbac", rbacCfg))
   337  	hcm.HttpFilters = append(hcm.HttpFilters, e2e.RouterHTTPFilter)
   338  
   339  	return &v3listenerpb.Listener{
   340  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
   341  		Address: &v3corepb.Address{
   342  			Address: &v3corepb.Address_SocketAddress{
   343  				SocketAddress: &v3corepb.SocketAddress{
   344  					Address: host,
   345  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   346  						PortValue: port,
   347  					},
   348  				},
   349  			},
   350  		},
   351  		FilterChains: []*v3listenerpb.FilterChain{
   352  			{
   353  				Name: "v4-wildcard",
   354  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   355  					PrefixRanges: []*v3corepb.CidrRange{
   356  						{
   357  							AddressPrefix: "0.0.0.0",
   358  							PrefixLen: &wrapperspb.UInt32Value{
   359  								Value: uint32(0),
   360  							},
   361  						},
   362  					},
   363  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   364  					SourcePrefixRanges: []*v3corepb.CidrRange{
   365  						{
   366  							AddressPrefix: "0.0.0.0",
   367  							PrefixLen: &wrapperspb.UInt32Value{
   368  								Value: uint32(0),
   369  							},
   370  						},
   371  					},
   372  				},
   373  				Filters: []*v3listenerpb.Filter{
   374  					{
   375  						Name: "filter-1",
   376  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   377  							TypedConfig: testutils.MarshalAny(t, hcm),
   378  						},
   379  					},
   380  				},
   381  			},
   382  			{
   383  				Name: "v6-wildcard",
   384  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   385  					PrefixRanges: []*v3corepb.CidrRange{
   386  						{
   387  							AddressPrefix: "::",
   388  							PrefixLen: &wrapperspb.UInt32Value{
   389  								Value: uint32(0),
   390  							},
   391  						},
   392  					},
   393  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   394  					SourcePrefixRanges: []*v3corepb.CidrRange{
   395  						{
   396  							AddressPrefix: "::",
   397  							PrefixLen: &wrapperspb.UInt32Value{
   398  								Value: uint32(0),
   399  							},
   400  						},
   401  					},
   402  				},
   403  				Filters: []*v3listenerpb.Filter{
   404  					{
   405  						Name: "filter-1",
   406  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   407  							TypedConfig: testutils.MarshalAny(t, hcm),
   408  						},
   409  					},
   410  				},
   411  			},
   412  		},
   413  	}
   414  }
   415  
   416  // TestRBACHTTPFilter tests the xds configured RBAC HTTP Filter. It sets up the
   417  // full end to end flow, and makes sure certain RPC's are successful and proceed
   418  // as normal and certain RPC's are denied by the RBAC HTTP Filter which gets
   419  // called by hooked xds interceptors.
   420  func (s) TestRBACHTTPFilter(t *testing.T) {
   421  	internal.RegisterRBACHTTPFilterForTesting()
   422  	defer internal.UnregisterRBACHTTPFilterForTesting()
   423  	tests := []struct {
   424  		name                string
   425  		rbacCfg             *rpb.RBAC
   426  		wantStatusEmptyCall codes.Code
   427  		wantStatusUnaryCall codes.Code
   428  		wantAuthzOutcomes   map[bool]int
   429  		eventContent        *audit.Event
   430  	}{
   431  		// This test tests an RBAC HTTP Filter which is configured to allow any RPC.
   432  		// Any RPC passing through this RBAC HTTP Filter should proceed as normal.
   433  		{
   434  			name: "allow-anything",
   435  			rbacCfg: &rpb.RBAC{
   436  				Rules: &v3rbacpb.RBAC{
   437  					Action: v3rbacpb.RBAC_ALLOW,
   438  					Policies: map[string]*v3rbacpb.Policy{
   439  						"anyone": {
   440  							Permissions: []*v3rbacpb.Permission{
   441  								{Rule: &v3rbacpb.Permission_Any{Any: true}},
   442  							},
   443  							Principals: []*v3rbacpb.Principal{
   444  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   445  							},
   446  						},
   447  					},
   448  					AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
   449  						AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
   450  						LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
   451  							{
   452  								AuditLogger: &v3corepb.TypedExtensionConfig{
   453  									Name:        "stat_logger",
   454  									TypedConfig: createXDSTypedStruct(t, map[string]any{}, "stat_logger"),
   455  								},
   456  								IsOptional: false,
   457  							},
   458  						},
   459  					},
   460  				},
   461  			},
   462  			wantStatusEmptyCall: codes.OK,
   463  			wantStatusUnaryCall: codes.OK,
   464  			wantAuthzOutcomes:   map[bool]int{true: 2, false: 0},
   465  			// TODO(gtcooke94) add policy name (RBAC filter name) once
   466  			// https://github.com/grpc/grpc-go/pull/6327 is merged.
   467  			eventContent: &audit.Event{
   468  				FullMethodName: "/grpc.testing.TestService/UnaryCall",
   469  				MatchedRule:    "anyone",
   470  				Authorized:     true,
   471  			},
   472  		},
   473  		// This test tests an RBAC HTTP Filter which is configured to allow only
   474  		// RPC's with certain paths ("UnaryCall"). Only unary calls passing
   475  		// through this RBAC HTTP Filter should proceed as normal, and any
   476  		// others should be denied.
   477  		{
   478  			name: "allow-certain-path",
   479  			rbacCfg: &rpb.RBAC{
   480  				Rules: &v3rbacpb.RBAC{
   481  					Action: v3rbacpb.RBAC_ALLOW,
   482  					Policies: map[string]*v3rbacpb.Policy{
   483  						"certain-path": {
   484  							Permissions: []*v3rbacpb.Permission{
   485  								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "/grpc.testing.TestService/UnaryCall"}}}}}},
   486  							},
   487  							Principals: []*v3rbacpb.Principal{
   488  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   489  							},
   490  						},
   491  					},
   492  				},
   493  			},
   494  			wantStatusEmptyCall: codes.PermissionDenied,
   495  			wantStatusUnaryCall: codes.OK,
   496  		},
   497  		// This test tests an RBAC HTTP Filter which is configured to allow only
   498  		// RPC's with certain paths ("UnaryCall") via the ":path" header. Only
   499  		// unary calls passing through this RBAC HTTP Filter should proceed as
   500  		// normal, and any others should be denied.
   501  		{
   502  			name: "allow-certain-path-by-header",
   503  			rbacCfg: &rpb.RBAC{
   504  				Rules: &v3rbacpb.RBAC{
   505  					Action: v3rbacpb.RBAC_ALLOW,
   506  					Policies: map[string]*v3rbacpb.Policy{
   507  						"certain-path": {
   508  							Permissions: []*v3rbacpb.Permission{
   509  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":path", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "/grpc.testing.TestService/UnaryCall"}}}},
   510  							},
   511  							Principals: []*v3rbacpb.Principal{
   512  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   513  							},
   514  						},
   515  					},
   516  				},
   517  			},
   518  			wantStatusEmptyCall: codes.PermissionDenied,
   519  			wantStatusUnaryCall: codes.OK,
   520  		},
   521  		// This test that a RBAC Config with nil rules means that every RPC is
   522  		// allowed. This maps to the line "If absent, no enforcing RBAC policy
   523  		// will be applied" from the RBAC Proto documentation for the Rules
   524  		// field.
   525  		{
   526  			name: "absent-rules",
   527  			rbacCfg: &rpb.RBAC{
   528  				Rules: nil,
   529  			},
   530  			wantStatusEmptyCall: codes.OK,
   531  			wantStatusUnaryCall: codes.OK,
   532  		},
   533  		// The two tests below test that configuring the xDS RBAC HTTP Filter
   534  		// with :authority and host header matchers end up being logically
   535  		// equivalent. This represents functionality from this line in A41 -
   536  		// "As documented for HeaderMatcher, Envoy aliases :authority and Host
   537  		// in its header map implementation, so they should be treated
   538  		// equivalent for the RBAC matchers; there must be no behavior change
   539  		// depending on which of the two header names is used in the RBAC
   540  		// policy."
   541  
   542  		// This test tests an xDS RBAC Filter with an :authority header matcher.
   543  		{
   544  			name: "match-on-authority",
   545  			rbacCfg: &rpb.RBAC{
   546  				Rules: &v3rbacpb.RBAC{
   547  					Action: v3rbacpb.RBAC_ALLOW,
   548  					Policies: map[string]*v3rbacpb.Policy{
   549  						"match-on-authority": {
   550  							Permissions: []*v3rbacpb.Permission{
   551  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":authority", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "my-service-fallback"}}}},
   552  							},
   553  							Principals: []*v3rbacpb.Principal{
   554  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   555  							},
   556  						},
   557  					},
   558  				},
   559  			},
   560  			wantStatusEmptyCall: codes.OK,
   561  			wantStatusUnaryCall: codes.OK,
   562  		},
   563  		// This test tests that configuring an xDS RBAC Filter with a host
   564  		// header matcher has the same behavior as if it was configured with
   565  		// :authority. Since host and authority are aliased, this should still
   566  		// continue to match on incoming RPC's :authority, just as the test
   567  		// above.
   568  		{
   569  			name: "match-on-host",
   570  			rbacCfg: &rpb.RBAC{
   571  				Rules: &v3rbacpb.RBAC{
   572  					Action: v3rbacpb.RBAC_ALLOW,
   573  					Policies: map[string]*v3rbacpb.Policy{
   574  						"match-on-authority": {
   575  							Permissions: []*v3rbacpb.Permission{
   576  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: "host", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "my-service-fallback"}}}},
   577  							},
   578  							Principals: []*v3rbacpb.Principal{
   579  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   580  							},
   581  						},
   582  					},
   583  				},
   584  			},
   585  			wantStatusEmptyCall: codes.OK,
   586  			wantStatusUnaryCall: codes.OK,
   587  		},
   588  		// This test tests that the RBAC HTTP Filter hard codes the :method
   589  		// header to POST. Since the RBAC Configuration says to deny every RPC
   590  		// with a method :POST, every RPC tried should be denied.
   591  		{
   592  			name: "deny-post",
   593  			rbacCfg: &rpb.RBAC{
   594  				Rules: &v3rbacpb.RBAC{
   595  					Action: v3rbacpb.RBAC_DENY,
   596  					Policies: map[string]*v3rbacpb.Policy{
   597  						"post-method": {
   598  							Permissions: []*v3rbacpb.Permission{
   599  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "POST"}}}},
   600  							},
   601  							Principals: []*v3rbacpb.Principal{
   602  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   603  							},
   604  						},
   605  					},
   606  				},
   607  			},
   608  			wantStatusEmptyCall: codes.PermissionDenied,
   609  			wantStatusUnaryCall: codes.PermissionDenied,
   610  		},
   611  		// This test tests that RBAC ignores the TE: trailers header (which is
   612  		// hardcoded in http2_client.go for every RPC). Since the RBAC
   613  		// Configuration says to only ALLOW RPC's with a TE: Trailers, every RPC
   614  		// tried should be denied.
   615  		{
   616  			name: "allow-only-te",
   617  			rbacCfg: &rpb.RBAC{
   618  				Rules: &v3rbacpb.RBAC{
   619  					Action: v3rbacpb.RBAC_ALLOW,
   620  					Policies: map[string]*v3rbacpb.Policy{
   621  						"post-method": {
   622  							Permissions: []*v3rbacpb.Permission{
   623  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: "TE", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "trailers"}}}},
   624  							},
   625  							Principals: []*v3rbacpb.Principal{
   626  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   627  							},
   628  						},
   629  					},
   630  				},
   631  			},
   632  			wantStatusEmptyCall: codes.PermissionDenied,
   633  			wantStatusUnaryCall: codes.PermissionDenied,
   634  		},
   635  		// This test tests that an RBAC Config with Action.LOG configured allows
   636  		// every RPC through. This maps to the line "At this time, if the
   637  		// RBAC.action is Action.LOG then the policy will be completely ignored,
   638  		// as if RBAC was not configured." from A41
   639  		{
   640  			name: "action-log",
   641  			rbacCfg: &rpb.RBAC{
   642  				Rules: &v3rbacpb.RBAC{
   643  					Action: v3rbacpb.RBAC_LOG,
   644  					Policies: map[string]*v3rbacpb.Policy{
   645  						"anyone": {
   646  							Permissions: []*v3rbacpb.Permission{
   647  								{Rule: &v3rbacpb.Permission_Any{Any: true}},
   648  							},
   649  							Principals: []*v3rbacpb.Principal{
   650  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   651  							},
   652  						},
   653  					},
   654  				},
   655  			},
   656  			wantStatusEmptyCall: codes.OK,
   657  			wantStatusUnaryCall: codes.OK,
   658  		},
   659  	}
   660  
   661  	for _, test := range tests {
   662  		t.Run(test.name, func(t *testing.T) {
   663  			func() {
   664  				lb := &loggerBuilder{
   665  					authzDecisionStat: map[bool]int{true: 0, false: 0},
   666  					lastEvent:         &audit.Event{},
   667  				}
   668  				audit.RegisterLoggerBuilder(lb)
   669  
   670  				managementServer, nodeID, bootstrapContents, xdsResolver := setup.ManagementServerAndResolver(t)
   671  
   672  				lis, cleanup2 := setupGRPCServer(t, bootstrapContents)
   673  				defer cleanup2()
   674  
   675  				host, port, err := hostPortFromListener(lis)
   676  				if err != nil {
   677  					t.Fatalf("failed to retrieve host and port of server: %v", err)
   678  				}
   679  				const serviceName = "my-service-fallback"
   680  				resources := e2e.DefaultClientResources(e2e.ResourceParams{
   681  					DialTarget: serviceName,
   682  					NodeID:     nodeID,
   683  					Host:       host,
   684  					Port:       port,
   685  					SecLevel:   e2e.SecurityLevelNone,
   686  				})
   687  				inboundLis := serverListenerWithRBACHTTPFilters(t, host, port, test.rbacCfg)
   688  				resources.Listeners = append(resources.Listeners, inboundLis)
   689  
   690  				ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   691  				defer cancel()
   692  				// Setup the management server with client and server-side resources.
   693  				if err := managementServer.Update(ctx, resources); err != nil {
   694  					t.Fatal(err)
   695  				}
   696  
   697  				cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   698  				if err != nil {
   699  					t.Fatalf("grpc.NewClient() failed: %v", err)
   700  				}
   701  				defer cc.Close()
   702  
   703  				client := testgrpc.NewTestServiceClient(cc)
   704  
   705  				if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); status.Code(err) != test.wantStatusEmptyCall {
   706  					t.Fatalf("EmptyCall() returned err with status: %v, wantStatusEmptyCall: %v", status.Code(err), test.wantStatusEmptyCall)
   707  				}
   708  
   709  				if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantStatusUnaryCall {
   710  					t.Fatalf("UnaryCall() returned err with status: %v, wantStatusUnaryCall: %v", err, test.wantStatusUnaryCall)
   711  				}
   712  
   713  				if test.wantAuthzOutcomes != nil {
   714  					if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" {
   715  						t.Fatalf("authorization decision do not match\ndiff (-got +want):\n%s", diff)
   716  					}
   717  				}
   718  				if test.eventContent != nil {
   719  					if diff := cmp.Diff(lb.lastEvent, test.eventContent); diff != "" {
   720  						t.Fatalf("unexpected event\ndiff (-got +want):\n%s", diff)
   721  					}
   722  				}
   723  			}()
   724  		})
   725  	}
   726  }
   727  
   728  // serverListenerWithBadRouteConfiguration returns an xds Listener resource with
   729  // a Route Configuration that will never successfully match in order to test
   730  // RBAC Environment variable being toggled on and off.
   731  func serverListenerWithBadRouteConfiguration(t *testing.T, host string, port uint32) *v3listenerpb.Listener {
   732  	return &v3listenerpb.Listener{
   733  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
   734  		Address: &v3corepb.Address{
   735  			Address: &v3corepb.Address_SocketAddress{
   736  				SocketAddress: &v3corepb.SocketAddress{
   737  					Address: host,
   738  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   739  						PortValue: port,
   740  					},
   741  				},
   742  			},
   743  		},
   744  		FilterChains: []*v3listenerpb.FilterChain{
   745  			{
   746  				Name: "v4-wildcard",
   747  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   748  					PrefixRanges: []*v3corepb.CidrRange{
   749  						{
   750  							AddressPrefix: "0.0.0.0",
   751  							PrefixLen: &wrapperspb.UInt32Value{
   752  								Value: uint32(0),
   753  							},
   754  						},
   755  					},
   756  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   757  					SourcePrefixRanges: []*v3corepb.CidrRange{
   758  						{
   759  							AddressPrefix: "0.0.0.0",
   760  							PrefixLen: &wrapperspb.UInt32Value{
   761  								Value: uint32(0),
   762  							},
   763  						},
   764  					},
   765  				},
   766  				Filters: []*v3listenerpb.Filter{
   767  					{
   768  						Name: "filter-1",
   769  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   770  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   771  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   772  									RouteConfig: &v3routepb.RouteConfiguration{
   773  										Name: "routeName",
   774  										VirtualHosts: []*v3routepb.VirtualHost{{
   775  											// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
   776  											// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
   777  											// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
   778  											// with this route configuration will be denied.
   779  											Domains: []string{"will-never-match"},
   780  											Routes: []*v3routepb.Route{{
   781  												Match: &v3routepb.RouteMatch{
   782  													PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   783  												},
   784  												Action: &v3routepb.Route_NonForwardingAction{},
   785  											}}}}},
   786  								},
   787  								HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   788  							}),
   789  						},
   790  					},
   791  				},
   792  			},
   793  			{
   794  				Name: "v6-wildcard",
   795  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   796  					PrefixRanges: []*v3corepb.CidrRange{
   797  						{
   798  							AddressPrefix: "::",
   799  							PrefixLen: &wrapperspb.UInt32Value{
   800  								Value: uint32(0),
   801  							},
   802  						},
   803  					},
   804  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   805  					SourcePrefixRanges: []*v3corepb.CidrRange{
   806  						{
   807  							AddressPrefix: "::",
   808  							PrefixLen: &wrapperspb.UInt32Value{
   809  								Value: uint32(0),
   810  							},
   811  						},
   812  					},
   813  				},
   814  				Filters: []*v3listenerpb.Filter{
   815  					{
   816  						Name: "filter-1",
   817  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   818  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   819  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   820  									RouteConfig: &v3routepb.RouteConfiguration{
   821  										Name: "routeName",
   822  										VirtualHosts: []*v3routepb.VirtualHost{{
   823  											// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
   824  											// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
   825  											// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
   826  											// with this route configuration will be denied.
   827  											Domains: []string{"will-never-match"},
   828  											Routes: []*v3routepb.Route{{
   829  												Match: &v3routepb.RouteMatch{
   830  													PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   831  												},
   832  												Action: &v3routepb.Route_NonForwardingAction{},
   833  											}}}}},
   834  								},
   835  								HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   836  							}),
   837  						},
   838  					},
   839  				},
   840  			},
   841  		},
   842  	}
   843  }
   844  
   845  func (s) TestRBAC_WithBadRouteConfiguration(t *testing.T) {
   846  	managementServer, nodeID, bootstrapContents, xdsResolver := setup.ManagementServerAndResolver(t)
   847  	// We need to wait for the server to enter SERVING mode before making RPCs
   848  	// to avoid flakes due to the server closing connections.
   849  	servingCh := make(chan struct{})
   850  
   851  	// Initialize a test gRPC server, assign it to the stub server, and start
   852  	// the test service.
   853  	opt := xds.ServingModeCallback(func(_ net.Addr, args xds.ServingModeChangeArgs) {
   854  		if args.Mode == connectivity.ServingModeServing {
   855  			close(servingCh)
   856  		}
   857  	})
   858  	lis, cleanup2 := setupGRPCServer(t, bootstrapContents, opt)
   859  	defer cleanup2()
   860  
   861  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   862  	defer cancel()
   863  
   864  	host, port, err := hostPortFromListener(lis)
   865  	if err != nil {
   866  		t.Fatalf("failed to retrieve host and port of server: %v", err)
   867  	}
   868  	const serviceName = "my-service-fallback"
   869  
   870  	// The inbound listener needs a route table that will never match on a VH,
   871  	// and thus shouldn't allow incoming RPC's to proceed.
   872  	resources := e2e.DefaultClientResources(e2e.ResourceParams{
   873  		DialTarget: serviceName,
   874  		NodeID:     nodeID,
   875  		Host:       host,
   876  		Port:       port,
   877  		SecLevel:   e2e.SecurityLevelNone,
   878  	})
   879  	// Since RBAC support is turned ON, all the RPC's should get denied with
   880  	// status code Unavailable due to not matching to a route of type Non
   881  	// Forwarding Action (Route Table not configured properly).
   882  	inboundLis := serverListenerWithBadRouteConfiguration(t, host, port)
   883  	resources.Listeners = append(resources.Listeners, inboundLis)
   884  
   885  	// Setup the management server with client and server-side resources.
   886  	if err := managementServer.Update(ctx, resources); err != nil {
   887  		t.Fatal(err)
   888  	}
   889  
   890  	select {
   891  	case <-ctx.Done():
   892  		t.Fatal("Timeout waiting for the xDS-enabled gRPC server to go SERVING")
   893  	case <-servingCh:
   894  	}
   895  
   896  	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   897  	if err != nil {
   898  		t.Fatalf("grpc.NewClient() failed: %v", err)
   899  	}
   900  	defer cc.Close()
   901  
   902  	client := testgrpc.NewTestServiceClient(cc)
   903  	_, err = client.EmptyCall(ctx, &testpb.Empty{})
   904  	if status.Code(err) != codes.Unavailable {
   905  		t.Fatalf("EmptyCall() returned %v, want Unavailable", err)
   906  	}
   907  	if !strings.Contains(err.Error(), nodeID) {
   908  		t.Fatalf("EmptyCall() = %v, want xDS node id %q", err, nodeID)
   909  	}
   910  	_, err = client.UnaryCall(ctx, &testpb.SimpleRequest{})
   911  	if status.Code(err) != codes.Unavailable {
   912  		t.Fatalf("UnaryCall() returned %v, want Unavailable", err)
   913  	}
   914  	if !strings.Contains(err.Error(), nodeID) {
   915  		t.Fatalf("UnaryCall() = %v, want xDS node id %q", err, nodeID)
   916  	}
   917  }
   918  
   919  type statAuditLogger struct {
   920  	authzDecisionStat map[bool]int // Map to hold counts of authorization decisions
   921  	lastEvent         *audit.Event // Field to store last received event
   922  }
   923  
   924  func (s *statAuditLogger) Log(event *audit.Event) {
   925  	s.authzDecisionStat[event.Authorized]++
   926  	*s.lastEvent = *event
   927  }
   928  
   929  type loggerBuilder struct {
   930  	authzDecisionStat map[bool]int
   931  	lastEvent         *audit.Event
   932  }
   933  
   934  func (loggerBuilder) Name() string {
   935  	return "stat_logger"
   936  }
   937  
   938  func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger {
   939  	return &statAuditLogger{
   940  		authzDecisionStat: lb.authzDecisionStat,
   941  		lastEvent:         lb.lastEvent,
   942  	}
   943  }
   944  
   945  func (*loggerBuilder) ParseLoggerConfig(json.RawMessage) (audit.LoggerConfig, error) {
   946  	return nil, nil
   947  }
   948  
   949  // This is used when converting a custom config from raw JSON to a TypedStruct.
   950  // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>".
   951  const typeURLPrefix = "grpc.authz.audit_logging/"
   952  
   953  // Builds custom configs for audit logger RBAC protos.
   954  func createXDSTypedStruct(t *testing.T, in map[string]any, name string) *anypb.Any {
   955  	t.Helper()
   956  	pb, err := structpb.NewStruct(in)
   957  	if err != nil {
   958  		t.Fatalf("createXDSTypedStruct failed during structpb.NewStruct: %v", err)
   959  	}
   960  	typedStruct := &v3xdsxdstypepb.TypedStruct{
   961  		TypeUrl: typeURLPrefix + name,
   962  		Value:   pb,
   963  	}
   964  	customConfig, err := anypb.New(typedStruct)
   965  	if err != nil {
   966  		t.Fatalf("createXDSTypedStruct failed during anypb.New: %v", err)
   967  	}
   968  	return customConfig
   969  }