...

Source file src/google.golang.org/grpc/test/xds/xds_server_rbac_test.go

Documentation: google.golang.org/grpc/test/xds

     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/status"
    41  	"google.golang.org/protobuf/types/known/anypb"
    42  	"google.golang.org/protobuf/types/known/structpb"
    43  	"google.golang.org/protobuf/types/known/wrapperspb"
    44  
    45  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    46  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    47  	v3rbacpb "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3"
    48  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    49  	rpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3"
    50  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    51  	v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
    52  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    53  	testpb "google.golang.org/grpc/interop/grpc_testing"
    54  )
    55  
    56  // TestServerSideXDS_RouteConfiguration is an e2e test which verifies routing
    57  // functionality. The xDS enabled server will be set up with route configuration
    58  // where the route configuration has routes with the correct routing actions
    59  // (NonForwardingAction), and the RPC's matching those routes should proceed as
    60  // normal.
    61  func (s) TestServerSideXDS_RouteConfiguration(t *testing.T) {
    62  	managementServer, nodeID, bootstrapContents, resolver, cleanup1 := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{})
    63  	defer cleanup1()
    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.DialContext(ctx, fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
   249  	if err != nil {
   250  		t.Fatalf("failed to dial local test server: %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  	if _, err = client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable {
   269  		t.Fatalf("client.UnaryCall() = _, %v, want _, error code %s", err, codes.Unavailable)
   270  	}
   271  
   272  	// This Streaming Call should match to a route with an incorrect action.
   273  	// Thus, this RPC should not go through as per A36, and this call should
   274  	// receive an error with codes.Unavailable.
   275  	stream, err := client.StreamingInputCall(ctx)
   276  	if err != nil {
   277  		t.Fatalf("StreamingInputCall(_) = _, %v, want <nil>", err)
   278  	}
   279  	if _, err = stream.CloseAndRecv(); status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), "the incoming RPC matched to a route that was not of action type non forwarding") {
   280  		t.Fatalf("streaming RPC should have been denied")
   281  	}
   282  
   283  	// This Full Duplex should not match to a route, and thus should return an
   284  	// error and not proceed.
   285  	dStream, err := client.FullDuplexCall(ctx)
   286  	if err != nil {
   287  		t.Fatalf("FullDuplexCall(_) = _, %v, want <nil>", err)
   288  	}
   289  	if _, err = dStream.Recv(); status.Code(err) != codes.Unavailable || !strings.Contains(err.Error(), "the incoming RPC did not match a configured Route") {
   290  		t.Fatalf("streaming RPC should have been denied")
   291  	}
   292  }
   293  
   294  // serverListenerWithRBACHTTPFilters returns an xds Listener resource with HTTP Filters defined in the HCM, and a route
   295  // configuration that always matches to a route and a VH.
   296  func serverListenerWithRBACHTTPFilters(t *testing.T, host string, port uint32, rbacCfg *rpb.RBAC) *v3listenerpb.Listener {
   297  	// Rather than declare typed config inline, take a HCM proto and append the
   298  	// RBAC Filters to it.
   299  	hcm := &v3httppb.HttpConnectionManager{
   300  		RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   301  			RouteConfig: &v3routepb.RouteConfiguration{
   302  				Name: "routeName",
   303  				VirtualHosts: []*v3routepb.VirtualHost{{
   304  					Domains: []string{"*"},
   305  					Routes: []*v3routepb.Route{{
   306  						Match: &v3routepb.RouteMatch{
   307  							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   308  						},
   309  						Action: &v3routepb.Route_NonForwardingAction{},
   310  					}},
   311  					// This tests override parsing + building when RBAC Filter
   312  					// passed both normal and override config.
   313  					TypedPerFilterConfig: map[string]*anypb.Any{
   314  						"rbac": testutils.MarshalAny(t, &rpb.RBACPerRoute{Rbac: rbacCfg}),
   315  					},
   316  				}}},
   317  		},
   318  	}
   319  	hcm.HttpFilters = nil
   320  	hcm.HttpFilters = append(hcm.HttpFilters, e2e.HTTPFilter("rbac", rbacCfg))
   321  	hcm.HttpFilters = append(hcm.HttpFilters, e2e.RouterHTTPFilter)
   322  
   323  	return &v3listenerpb.Listener{
   324  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
   325  		Address: &v3corepb.Address{
   326  			Address: &v3corepb.Address_SocketAddress{
   327  				SocketAddress: &v3corepb.SocketAddress{
   328  					Address: host,
   329  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   330  						PortValue: port,
   331  					},
   332  				},
   333  			},
   334  		},
   335  		FilterChains: []*v3listenerpb.FilterChain{
   336  			{
   337  				Name: "v4-wildcard",
   338  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   339  					PrefixRanges: []*v3corepb.CidrRange{
   340  						{
   341  							AddressPrefix: "0.0.0.0",
   342  							PrefixLen: &wrapperspb.UInt32Value{
   343  								Value: uint32(0),
   344  							},
   345  						},
   346  					},
   347  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   348  					SourcePrefixRanges: []*v3corepb.CidrRange{
   349  						{
   350  							AddressPrefix: "0.0.0.0",
   351  							PrefixLen: &wrapperspb.UInt32Value{
   352  								Value: uint32(0),
   353  							},
   354  						},
   355  					},
   356  				},
   357  				Filters: []*v3listenerpb.Filter{
   358  					{
   359  						Name: "filter-1",
   360  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   361  							TypedConfig: testutils.MarshalAny(t, hcm),
   362  						},
   363  					},
   364  				},
   365  			},
   366  			{
   367  				Name: "v6-wildcard",
   368  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   369  					PrefixRanges: []*v3corepb.CidrRange{
   370  						{
   371  							AddressPrefix: "::",
   372  							PrefixLen: &wrapperspb.UInt32Value{
   373  								Value: uint32(0),
   374  							},
   375  						},
   376  					},
   377  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   378  					SourcePrefixRanges: []*v3corepb.CidrRange{
   379  						{
   380  							AddressPrefix: "::",
   381  							PrefixLen: &wrapperspb.UInt32Value{
   382  								Value: uint32(0),
   383  							},
   384  						},
   385  					},
   386  				},
   387  				Filters: []*v3listenerpb.Filter{
   388  					{
   389  						Name: "filter-1",
   390  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   391  							TypedConfig: testutils.MarshalAny(t, hcm),
   392  						},
   393  					},
   394  				},
   395  			},
   396  		},
   397  	}
   398  }
   399  
   400  // TestRBACHTTPFilter tests the xds configured RBAC HTTP Filter. It sets up the
   401  // full end to end flow, and makes sure certain RPC's are successful and proceed
   402  // as normal and certain RPC's are denied by the RBAC HTTP Filter which gets
   403  // called by hooked xds interceptors.
   404  func (s) TestRBACHTTPFilter(t *testing.T) {
   405  	internal.RegisterRBACHTTPFilterForTesting()
   406  	defer internal.UnregisterRBACHTTPFilterForTesting()
   407  	tests := []struct {
   408  		name                string
   409  		rbacCfg             *rpb.RBAC
   410  		wantStatusEmptyCall codes.Code
   411  		wantStatusUnaryCall codes.Code
   412  		wantAuthzOutcomes   map[bool]int
   413  		eventContent        *audit.Event
   414  	}{
   415  		// This test tests an RBAC HTTP Filter which is configured to allow any RPC.
   416  		// Any RPC passing through this RBAC HTTP Filter should proceed as normal.
   417  		{
   418  			name: "allow-anything",
   419  			rbacCfg: &rpb.RBAC{
   420  				Rules: &v3rbacpb.RBAC{
   421  					Action: v3rbacpb.RBAC_ALLOW,
   422  					Policies: map[string]*v3rbacpb.Policy{
   423  						"anyone": {
   424  							Permissions: []*v3rbacpb.Permission{
   425  								{Rule: &v3rbacpb.Permission_Any{Any: true}},
   426  							},
   427  							Principals: []*v3rbacpb.Principal{
   428  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   429  							},
   430  						},
   431  					},
   432  					AuditLoggingOptions: &v3rbacpb.RBAC_AuditLoggingOptions{
   433  						AuditCondition: v3rbacpb.RBAC_AuditLoggingOptions_ON_ALLOW,
   434  						LoggerConfigs: []*v3rbacpb.RBAC_AuditLoggingOptions_AuditLoggerConfig{
   435  							{
   436  								AuditLogger: &v3corepb.TypedExtensionConfig{
   437  									Name:        "stat_logger",
   438  									TypedConfig: createXDSTypedStruct(t, map[string]any{}, "stat_logger"),
   439  								},
   440  								IsOptional: false,
   441  							},
   442  						},
   443  					},
   444  				},
   445  			},
   446  			wantStatusEmptyCall: codes.OK,
   447  			wantStatusUnaryCall: codes.OK,
   448  			wantAuthzOutcomes:   map[bool]int{true: 2, false: 0},
   449  			// TODO(gtcooke94) add policy name (RBAC filter name) once
   450  			// https://github.com/grpc/grpc-go/pull/6327 is merged.
   451  			eventContent: &audit.Event{
   452  				FullMethodName: "/grpc.testing.TestService/UnaryCall",
   453  				MatchedRule:    "anyone",
   454  				Authorized:     true,
   455  			},
   456  		},
   457  		// This test tests an RBAC HTTP Filter which is configured to allow only
   458  		// RPC's with certain paths ("UnaryCall"). Only unary calls passing
   459  		// through this RBAC HTTP Filter should proceed as normal, and any
   460  		// others should be denied.
   461  		{
   462  			name: "allow-certain-path",
   463  			rbacCfg: &rpb.RBAC{
   464  				Rules: &v3rbacpb.RBAC{
   465  					Action: v3rbacpb.RBAC_ALLOW,
   466  					Policies: map[string]*v3rbacpb.Policy{
   467  						"certain-path": {
   468  							Permissions: []*v3rbacpb.Permission{
   469  								{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "/grpc.testing.TestService/UnaryCall"}}}}}},
   470  							},
   471  							Principals: []*v3rbacpb.Principal{
   472  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   473  							},
   474  						},
   475  					},
   476  				},
   477  			},
   478  			wantStatusEmptyCall: codes.PermissionDenied,
   479  			wantStatusUnaryCall: codes.OK,
   480  		},
   481  		// This test that a RBAC Config with nil rules means that every RPC is
   482  		// allowed. This maps to the line "If absent, no enforcing RBAC policy
   483  		// will be applied" from the RBAC Proto documentation for the Rules
   484  		// field.
   485  		{
   486  			name: "absent-rules",
   487  			rbacCfg: &rpb.RBAC{
   488  				Rules: nil,
   489  			},
   490  			wantStatusEmptyCall: codes.OK,
   491  			wantStatusUnaryCall: codes.OK,
   492  		},
   493  		// The two tests below test that configuring the xDS RBAC HTTP Filter
   494  		// with :authority and host header matchers end up being logically
   495  		// equivalent. This represents functionality from this line in A41 -
   496  		// "As documented for HeaderMatcher, Envoy aliases :authority and Host
   497  		// in its header map implementation, so they should be treated
   498  		// equivalent for the RBAC matchers; there must be no behavior change
   499  		// depending on which of the two header names is used in the RBAC
   500  		// policy."
   501  
   502  		// This test tests an xDS RBAC Filter with an :authority header matcher.
   503  		{
   504  			name: "match-on-authority",
   505  			rbacCfg: &rpb.RBAC{
   506  				Rules: &v3rbacpb.RBAC{
   507  					Action: v3rbacpb.RBAC_ALLOW,
   508  					Policies: map[string]*v3rbacpb.Policy{
   509  						"match-on-authority": {
   510  							Permissions: []*v3rbacpb.Permission{
   511  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":authority", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "my-service-fallback"}}}},
   512  							},
   513  							Principals: []*v3rbacpb.Principal{
   514  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   515  							},
   516  						},
   517  					},
   518  				},
   519  			},
   520  			wantStatusEmptyCall: codes.OK,
   521  			wantStatusUnaryCall: codes.OK,
   522  		},
   523  		// This test tests that configuring an xDS RBAC Filter with a host
   524  		// header matcher has the same behavior as if it was configured with
   525  		// :authority. Since host and authority are aliased, this should still
   526  		// continue to match on incoming RPC's :authority, just as the test
   527  		// above.
   528  		{
   529  			name: "match-on-host",
   530  			rbacCfg: &rpb.RBAC{
   531  				Rules: &v3rbacpb.RBAC{
   532  					Action: v3rbacpb.RBAC_ALLOW,
   533  					Policies: map[string]*v3rbacpb.Policy{
   534  						"match-on-authority": {
   535  							Permissions: []*v3rbacpb.Permission{
   536  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: "host", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "my-service-fallback"}}}},
   537  							},
   538  							Principals: []*v3rbacpb.Principal{
   539  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   540  							},
   541  						},
   542  					},
   543  				},
   544  			},
   545  			wantStatusEmptyCall: codes.OK,
   546  			wantStatusUnaryCall: codes.OK,
   547  		},
   548  		// This test tests that the RBAC HTTP Filter hard codes the :method
   549  		// header to POST. Since the RBAC Configuration says to deny every RPC
   550  		// with a method :POST, every RPC tried should be denied.
   551  		{
   552  			name: "deny-post",
   553  			rbacCfg: &rpb.RBAC{
   554  				Rules: &v3rbacpb.RBAC{
   555  					Action: v3rbacpb.RBAC_DENY,
   556  					Policies: map[string]*v3rbacpb.Policy{
   557  						"post-method": {
   558  							Permissions: []*v3rbacpb.Permission{
   559  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "POST"}}}},
   560  							},
   561  							Principals: []*v3rbacpb.Principal{
   562  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   563  							},
   564  						},
   565  					},
   566  				},
   567  			},
   568  			wantStatusEmptyCall: codes.PermissionDenied,
   569  			wantStatusUnaryCall: codes.PermissionDenied,
   570  		},
   571  		// This test tests that RBAC ignores the TE: trailers header (which is
   572  		// hardcoded in http2_client.go for every RPC). Since the RBAC
   573  		// Configuration says to only ALLOW RPC's with a TE: Trailers, every RPC
   574  		// tried should be denied.
   575  		{
   576  			name: "allow-only-te",
   577  			rbacCfg: &rpb.RBAC{
   578  				Rules: &v3rbacpb.RBAC{
   579  					Action: v3rbacpb.RBAC_ALLOW,
   580  					Policies: map[string]*v3rbacpb.Policy{
   581  						"post-method": {
   582  							Permissions: []*v3rbacpb.Permission{
   583  								{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: "TE", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "trailers"}}}},
   584  							},
   585  							Principals: []*v3rbacpb.Principal{
   586  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   587  							},
   588  						},
   589  					},
   590  				},
   591  			},
   592  			wantStatusEmptyCall: codes.PermissionDenied,
   593  			wantStatusUnaryCall: codes.PermissionDenied,
   594  		},
   595  		// This test tests that an RBAC Config with Action.LOG configured allows
   596  		// every RPC through. This maps to the line "At this time, if the
   597  		// RBAC.action is Action.LOG then the policy will be completely ignored,
   598  		// as if RBAC was not configurated." from A41
   599  		{
   600  			name: "action-log",
   601  			rbacCfg: &rpb.RBAC{
   602  				Rules: &v3rbacpb.RBAC{
   603  					Action: v3rbacpb.RBAC_LOG,
   604  					Policies: map[string]*v3rbacpb.Policy{
   605  						"anyone": {
   606  							Permissions: []*v3rbacpb.Permission{
   607  								{Rule: &v3rbacpb.Permission_Any{Any: true}},
   608  							},
   609  							Principals: []*v3rbacpb.Principal{
   610  								{Identifier: &v3rbacpb.Principal_Any{Any: true}},
   611  							},
   612  						},
   613  					},
   614  				},
   615  			},
   616  			wantStatusEmptyCall: codes.OK,
   617  			wantStatusUnaryCall: codes.OK,
   618  		},
   619  	}
   620  
   621  	for _, test := range tests {
   622  		t.Run(test.name, func(t *testing.T) {
   623  			func() {
   624  				lb := &loggerBuilder{
   625  					authzDecisionStat: map[bool]int{true: 0, false: 0},
   626  					lastEvent:         &audit.Event{},
   627  				}
   628  				audit.RegisterLoggerBuilder(lb)
   629  
   630  				managementServer, nodeID, bootstrapContents, resolver, cleanup1 := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{})
   631  				defer cleanup1()
   632  
   633  				lis, cleanup2 := setupGRPCServer(t, bootstrapContents)
   634  				defer cleanup2()
   635  
   636  				host, port, err := hostPortFromListener(lis)
   637  				if err != nil {
   638  					t.Fatalf("failed to retrieve host and port of server: %v", err)
   639  				}
   640  				const serviceName = "my-service-fallback"
   641  				resources := e2e.DefaultClientResources(e2e.ResourceParams{
   642  					DialTarget: serviceName,
   643  					NodeID:     nodeID,
   644  					Host:       host,
   645  					Port:       port,
   646  					SecLevel:   e2e.SecurityLevelNone,
   647  				})
   648  				inboundLis := serverListenerWithRBACHTTPFilters(t, host, port, test.rbacCfg)
   649  				resources.Listeners = append(resources.Listeners, inboundLis)
   650  
   651  				ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   652  				defer cancel()
   653  				// Setup the management server with client and server-side resources.
   654  				if err := managementServer.Update(ctx, resources); err != nil {
   655  					t.Fatal(err)
   656  				}
   657  
   658  				cc, err := grpc.DialContext(ctx, fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
   659  				if err != nil {
   660  					t.Fatalf("failed to dial local test server: %v", err)
   661  				}
   662  				defer cc.Close()
   663  
   664  				client := testgrpc.NewTestServiceClient(cc)
   665  
   666  				if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); status.Code(err) != test.wantStatusEmptyCall {
   667  					t.Fatalf("EmptyCall() returned err with status: %v, wantStatusEmptyCall: %v", status.Code(err), test.wantStatusEmptyCall)
   668  				}
   669  
   670  				if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != test.wantStatusUnaryCall {
   671  					t.Fatalf("UnaryCall() returned err with status: %v, wantStatusUnaryCall: %v", err, test.wantStatusUnaryCall)
   672  				}
   673  
   674  				if test.wantAuthzOutcomes != nil {
   675  					if diff := cmp.Diff(lb.authzDecisionStat, test.wantAuthzOutcomes); diff != "" {
   676  						t.Fatalf("authorization decision do not match\ndiff (-got +want):\n%s", diff)
   677  					}
   678  				}
   679  				if test.eventContent != nil {
   680  					if diff := cmp.Diff(lb.lastEvent, test.eventContent); diff != "" {
   681  						t.Fatalf("unexpected event\ndiff (-got +want):\n%s", diff)
   682  					}
   683  				}
   684  			}()
   685  		})
   686  	}
   687  }
   688  
   689  // serverListenerWithBadRouteConfiguration returns an xds Listener resource with
   690  // a Route Configuration that will never successfully match in order to test
   691  // RBAC Environment variable being toggled on and off.
   692  func serverListenerWithBadRouteConfiguration(t *testing.T, host string, port uint32) *v3listenerpb.Listener {
   693  	return &v3listenerpb.Listener{
   694  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host, strconv.Itoa(int(port)))),
   695  		Address: &v3corepb.Address{
   696  			Address: &v3corepb.Address_SocketAddress{
   697  				SocketAddress: &v3corepb.SocketAddress{
   698  					Address: host,
   699  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   700  						PortValue: port,
   701  					},
   702  				},
   703  			},
   704  		},
   705  		FilterChains: []*v3listenerpb.FilterChain{
   706  			{
   707  				Name: "v4-wildcard",
   708  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   709  					PrefixRanges: []*v3corepb.CidrRange{
   710  						{
   711  							AddressPrefix: "0.0.0.0",
   712  							PrefixLen: &wrapperspb.UInt32Value{
   713  								Value: uint32(0),
   714  							},
   715  						},
   716  					},
   717  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   718  					SourcePrefixRanges: []*v3corepb.CidrRange{
   719  						{
   720  							AddressPrefix: "0.0.0.0",
   721  							PrefixLen: &wrapperspb.UInt32Value{
   722  								Value: uint32(0),
   723  							},
   724  						},
   725  					},
   726  				},
   727  				Filters: []*v3listenerpb.Filter{
   728  					{
   729  						Name: "filter-1",
   730  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   731  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   732  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   733  									RouteConfig: &v3routepb.RouteConfiguration{
   734  										Name: "routeName",
   735  										VirtualHosts: []*v3routepb.VirtualHost{{
   736  											// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
   737  											// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
   738  											// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
   739  											// with this route configuration will be denied.
   740  											Domains: []string{"will-never-match"},
   741  											Routes: []*v3routepb.Route{{
   742  												Match: &v3routepb.RouteMatch{
   743  													PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   744  												},
   745  												Action: &v3routepb.Route_NonForwardingAction{},
   746  											}}}}},
   747  								},
   748  								HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   749  							}),
   750  						},
   751  					},
   752  				},
   753  			},
   754  			{
   755  				Name: "v6-wildcard",
   756  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   757  					PrefixRanges: []*v3corepb.CidrRange{
   758  						{
   759  							AddressPrefix: "::",
   760  							PrefixLen: &wrapperspb.UInt32Value{
   761  								Value: uint32(0),
   762  							},
   763  						},
   764  					},
   765  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   766  					SourcePrefixRanges: []*v3corepb.CidrRange{
   767  						{
   768  							AddressPrefix: "::",
   769  							PrefixLen: &wrapperspb.UInt32Value{
   770  								Value: uint32(0),
   771  							},
   772  						},
   773  					},
   774  				},
   775  				Filters: []*v3listenerpb.Filter{
   776  					{
   777  						Name: "filter-1",
   778  						ConfigType: &v3listenerpb.Filter_TypedConfig{
   779  							TypedConfig: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
   780  								RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   781  									RouteConfig: &v3routepb.RouteConfiguration{
   782  										Name: "routeName",
   783  										VirtualHosts: []*v3routepb.VirtualHost{{
   784  											// Incoming RPC's will try and match to Virtual Hosts based on their :authority header.
   785  											// Thus, incoming RPC's will never match to a Virtual Host (server side requires matching
   786  											// to a VH/Route of type Non Forwarding Action to proceed normally), and all incoming RPC's
   787  											// with this route configuration will be denied.
   788  											Domains: []string{"will-never-match"},
   789  											Routes: []*v3routepb.Route{{
   790  												Match: &v3routepb.RouteMatch{
   791  													PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   792  												},
   793  												Action: &v3routepb.Route_NonForwardingAction{},
   794  											}}}}},
   795  								},
   796  								HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   797  							}),
   798  						},
   799  					},
   800  				},
   801  			},
   802  		},
   803  	}
   804  }
   805  
   806  func (s) TestRBACToggledOn_WithBadRouteConfiguration(t *testing.T) {
   807  	managementServer, nodeID, bootstrapContents, resolver, cleanup1 := e2e.SetupManagementServer(t, e2e.ManagementServerOptions{})
   808  	defer cleanup1()
   809  
   810  	lis, cleanup2 := setupGRPCServer(t, bootstrapContents)
   811  	defer cleanup2()
   812  
   813  	host, port, err := hostPortFromListener(lis)
   814  	if err != nil {
   815  		t.Fatalf("failed to retrieve host and port of server: %v", err)
   816  	}
   817  	const serviceName = "my-service-fallback"
   818  
   819  	// The inbound listener needs a route table that will never match on a VH,
   820  	// and thus shouldn't allow incoming RPC's to proceed.
   821  	resources := e2e.DefaultClientResources(e2e.ResourceParams{
   822  		DialTarget: serviceName,
   823  		NodeID:     nodeID,
   824  		Host:       host,
   825  		Port:       port,
   826  		SecLevel:   e2e.SecurityLevelNone,
   827  	})
   828  	// Since RBAC support is turned ON, all the RPC's should get denied with
   829  	// status code Unavailable due to not matching to a route of type Non
   830  	// Forwarding Action (Route Table not configured properly).
   831  	inboundLis := serverListenerWithBadRouteConfiguration(t, host, port)
   832  	resources.Listeners = append(resources.Listeners, inboundLis)
   833  
   834  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   835  	defer cancel()
   836  	// Setup the management server with client and server-side resources.
   837  	if err := managementServer.Update(ctx, resources); err != nil {
   838  		t.Fatal(err)
   839  	}
   840  
   841  	cc, err := grpc.DialContext(ctx, fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
   842  	if err != nil {
   843  		t.Fatalf("failed to dial local test server: %v", err)
   844  	}
   845  	defer cc.Close()
   846  
   847  	client := testgrpc.NewTestServiceClient(cc)
   848  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); status.Code(err) != codes.Unavailable {
   849  		t.Fatalf("EmptyCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
   850  	}
   851  	if _, err := client.UnaryCall(ctx, &testpb.SimpleRequest{}); status.Code(err) != codes.Unavailable {
   852  		t.Fatalf("UnaryCall() returned err with status: %v, if RBAC is disabled all RPC's should proceed as normal", status.Code(err))
   853  	}
   854  }
   855  
   856  type statAuditLogger struct {
   857  	authzDecisionStat map[bool]int // Map to hold counts of authorization decisions
   858  	lastEvent         *audit.Event // Field to store last received event
   859  }
   860  
   861  func (s *statAuditLogger) Log(event *audit.Event) {
   862  	s.authzDecisionStat[event.Authorized]++
   863  	*s.lastEvent = *event
   864  }
   865  
   866  type loggerBuilder struct {
   867  	authzDecisionStat map[bool]int
   868  	lastEvent         *audit.Event
   869  }
   870  
   871  func (loggerBuilder) Name() string {
   872  	return "stat_logger"
   873  }
   874  
   875  func (lb *loggerBuilder) Build(audit.LoggerConfig) audit.Logger {
   876  	return &statAuditLogger{
   877  		authzDecisionStat: lb.authzDecisionStat,
   878  		lastEvent:         lb.lastEvent,
   879  	}
   880  }
   881  
   882  func (*loggerBuilder) ParseLoggerConfig(config json.RawMessage) (audit.LoggerConfig, error) {
   883  	return nil, nil
   884  }
   885  
   886  // This is used when converting a custom config from raw JSON to a TypedStruct.
   887  // The TypeURL of the TypeStruct will be "grpc.authz.audit_logging/<name>".
   888  const typeURLPrefix = "grpc.authz.audit_logging/"
   889  
   890  // Builds custom configs for audit logger RBAC protos.
   891  func createXDSTypedStruct(t *testing.T, in map[string]any, name string) *anypb.Any {
   892  	t.Helper()
   893  	pb, err := structpb.NewStruct(in)
   894  	if err != nil {
   895  		t.Fatalf("createXDSTypedStruct failed during structpb.NewStruct: %v", err)
   896  	}
   897  	typedStruct := &v3xdsxdstypepb.TypedStruct{
   898  		TypeUrl: typeURLPrefix + name,
   899  		Value:   pb,
   900  	}
   901  	customConfig, err := anypb.New(typedStruct)
   902  	if err != nil {
   903  		t.Fatalf("createXDSTypedStruct failed during anypb.New: %v", err)
   904  	}
   905  	return customConfig
   906  }
   907  

View as plain text