google.golang.org/grpc@v1.62.1/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/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 }