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