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