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 }