gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/grpc/internal/xds/rbac/rbac_engine_test.go (about) 1 /* 2 * Copyright 2021 gRPC authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package rbac 18 19 import ( 20 "context" 21 "crypto/x509/pkix" 22 "net" 23 "net/url" 24 "testing" 25 26 "gitee.com/ks-custle/core-gm/x509" 27 28 tls "gitee.com/ks-custle/core-gm/gmtls" 29 30 v3corepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/core/v3" 31 v3rbacpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/rbac/v3" 32 v3routepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/config/route/v3" 33 v3matcherpb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/type/matcher/v3" 34 v3typepb "gitee.com/ks-custle/core-gm/go-control-plane/envoy/type/v3" 35 grpc "gitee.com/ks-custle/core-gm/grpc" 36 "gitee.com/ks-custle/core-gm/grpc/codes" 37 "gitee.com/ks-custle/core-gm/grpc/credentials" 38 "gitee.com/ks-custle/core-gm/grpc/internal/grpctest" 39 "gitee.com/ks-custle/core-gm/grpc/metadata" 40 "gitee.com/ks-custle/core-gm/grpc/peer" 41 "gitee.com/ks-custle/core-gm/grpc/status" 42 wrapperspb "github.com/golang/protobuf/ptypes/wrappers" 43 ) 44 45 type s struct { 46 grpctest.Tester 47 } 48 49 func Test(t *testing.T) { 50 grpctest.RunSubTests(t, s{}) 51 } 52 53 type addr struct { 54 ipAddress string 55 } 56 57 func (addr) Network() string { return "" } 58 func (a *addr) String() string { return a.ipAddress } 59 60 // TestNewChainEngine tests the construction of the ChainEngine. Due to some 61 // types of RBAC configuration being logically wrong and returning an error 62 // rather than successfully constructing the RBAC Engine, this test tests both 63 // RBAC Configurations deemed successful and also RBAC Configurations that will 64 // raise errors. 65 func (s) TestNewChainEngine(t *testing.T) { 66 tests := []struct { 67 name string 68 policies []*v3rbacpb.RBAC 69 wantErr bool 70 }{ 71 { 72 name: "SuccessCaseAnyMatchSingular", 73 policies: []*v3rbacpb.RBAC{ 74 { 75 Action: v3rbacpb.RBAC_ALLOW, 76 Policies: map[string]*v3rbacpb.Policy{ 77 "anyone": { 78 Permissions: []*v3rbacpb.Permission{ 79 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 80 }, 81 Principals: []*v3rbacpb.Principal{ 82 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 83 }, 84 }, 85 }, 86 }, 87 }, 88 }, 89 { 90 name: "SuccessCaseAnyMatchMultiple", 91 policies: []*v3rbacpb.RBAC{ 92 { 93 Action: v3rbacpb.RBAC_ALLOW, 94 Policies: map[string]*v3rbacpb.Policy{ 95 "anyone": { 96 Permissions: []*v3rbacpb.Permission{ 97 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 98 }, 99 Principals: []*v3rbacpb.Principal{ 100 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 101 }, 102 }, 103 }, 104 }, 105 { 106 Action: v3rbacpb.RBAC_DENY, 107 Policies: map[string]*v3rbacpb.Policy{ 108 "anyone": { 109 Permissions: []*v3rbacpb.Permission{ 110 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 111 }, 112 Principals: []*v3rbacpb.Principal{ 113 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 114 }, 115 }, 116 }, 117 }, 118 }, 119 }, 120 { 121 name: "SuccessCaseSimplePolicySingular", 122 policies: []*v3rbacpb.RBAC{ 123 { 124 Action: v3rbacpb.RBAC_ALLOW, 125 Policies: map[string]*v3rbacpb.Policy{ 126 "localhost-fan": { 127 Permissions: []*v3rbacpb.Permission{ 128 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}}, 129 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 130 }, 131 Principals: []*v3rbacpb.Principal{ 132 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 133 }, 134 }, 135 }, 136 }, 137 }, 138 }, 139 // SuccessCaseSimplePolicyTwoPolicies tests the construction of the 140 // chained engines in the case where there are two policies in a list, 141 // one with an allow policy and one with a deny policy. A situation 142 // where two policies (allow and deny) is a very common use case for 143 // this API, and should successfully build. 144 { 145 name: "SuccessCaseSimplePolicyTwoPolicies", 146 policies: []*v3rbacpb.RBAC{ 147 { 148 Action: v3rbacpb.RBAC_ALLOW, 149 Policies: map[string]*v3rbacpb.Policy{ 150 "localhost-fan": { 151 Permissions: []*v3rbacpb.Permission{ 152 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}}, 153 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 154 }, 155 Principals: []*v3rbacpb.Principal{ 156 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 157 }, 158 }, 159 }, 160 }, 161 { 162 Action: v3rbacpb.RBAC_DENY, 163 Policies: map[string]*v3rbacpb.Policy{ 164 "localhost-fan": { 165 Permissions: []*v3rbacpb.Permission{ 166 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 8080}}, 167 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 168 }, 169 Principals: []*v3rbacpb.Principal{ 170 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 171 }, 172 }, 173 }, 174 }, 175 }, 176 }, 177 { 178 name: "SuccessCaseEnvoyExampleSingular", 179 policies: []*v3rbacpb.RBAC{ 180 { 181 Action: v3rbacpb.RBAC_ALLOW, 182 Policies: map[string]*v3rbacpb.Policy{ 183 "service-admin": { 184 Permissions: []*v3rbacpb.Permission{ 185 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 186 }, 187 Principals: []*v3rbacpb.Principal{ 188 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/admin"}}}}}, 189 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "cluster.local/ns/default/sa/superuser"}}}}}, 190 }, 191 }, 192 "product-viewer": { 193 Permissions: []*v3rbacpb.Permission{ 194 {Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ 195 Rules: []*v3rbacpb.Permission{ 196 {Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 197 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}}, 198 {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ 199 Rules: []*v3rbacpb.Permission{ 200 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 80}}, 201 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 443}}, 202 }, 203 }, 204 }, 205 }, 206 }, 207 }, 208 }, 209 }, 210 }, 211 Principals: []*v3rbacpb.Principal{ 212 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 213 }, 214 }, 215 }, 216 }, 217 }, 218 }, 219 { 220 name: "SourceIpMatcherSuccessSingular", 221 policies: []*v3rbacpb.RBAC{ 222 { 223 Action: v3rbacpb.RBAC_ALLOW, 224 Policies: map[string]*v3rbacpb.Policy{ 225 "certain-source-ip": { 226 Permissions: []*v3rbacpb.Permission{ 227 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 228 }, 229 Principals: []*v3rbacpb.Principal{ 230 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 231 }, 232 }, 233 }, 234 }, 235 }, 236 }, 237 { 238 name: "SourceIpMatcherFailureSingular", 239 policies: []*v3rbacpb.RBAC{ 240 { 241 Action: v3rbacpb.RBAC_ALLOW, 242 Policies: map[string]*v3rbacpb.Policy{ 243 "certain-source-ip": { 244 Permissions: []*v3rbacpb.Permission{ 245 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 246 }, 247 Principals: []*v3rbacpb.Principal{ 248 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 249 }, 250 }, 251 }, 252 }, 253 }, 254 wantErr: true, 255 }, 256 { 257 name: "DestinationIpMatcherSuccess", 258 policies: []*v3rbacpb.RBAC{ 259 { 260 Action: v3rbacpb.RBAC_ALLOW, 261 Policies: map[string]*v3rbacpb.Policy{ 262 "certain-destination-ip": { 263 Permissions: []*v3rbacpb.Permission{ 264 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 265 }, 266 Principals: []*v3rbacpb.Principal{ 267 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 268 }, 269 }, 270 }, 271 }, 272 }, 273 }, 274 { 275 name: "DestinationIpMatcherFailure", 276 policies: []*v3rbacpb.RBAC{ 277 { 278 Action: v3rbacpb.RBAC_ALLOW, 279 Policies: map[string]*v3rbacpb.Policy{ 280 "certain-destination-ip": { 281 Permissions: []*v3rbacpb.Permission{ 282 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 283 }, 284 Principals: []*v3rbacpb.Principal{ 285 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 286 }, 287 }, 288 }, 289 }, 290 }, 291 wantErr: true, 292 }, 293 { 294 name: "MatcherToNotPolicy", 295 policies: []*v3rbacpb.RBAC{ 296 { 297 Action: v3rbacpb.RBAC_ALLOW, 298 Policies: map[string]*v3rbacpb.Policy{ 299 "not-secret-content": { 300 Permissions: []*v3rbacpb.Permission{ 301 {Rule: &v3rbacpb.Permission_NotRule{NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}}}}, 302 }, 303 Principals: []*v3rbacpb.Principal{ 304 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 305 }, 306 }, 307 }, 308 }, 309 }, 310 }, 311 { 312 name: "MatcherToNotPrinicipal", 313 policies: []*v3rbacpb.RBAC{ 314 { 315 Action: v3rbacpb.RBAC_ALLOW, 316 Policies: map[string]*v3rbacpb.Policy{ 317 "not-from-certain-ip": { 318 Permissions: []*v3rbacpb.Permission{ 319 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 320 }, 321 Principals: []*v3rbacpb.Principal{ 322 {Identifier: &v3rbacpb.Principal_NotId{NotId: &v3rbacpb.Principal{Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}}}, 323 }, 324 }, 325 }, 326 }, 327 }, 328 }, 329 // PrinicpalProductViewer tests the construction of a chained engine 330 // with a policy that allows any downstream to send a GET request on a 331 // certain path. 332 { 333 name: "PrincipalProductViewer", 334 policies: []*v3rbacpb.RBAC{ 335 { 336 Action: v3rbacpb.RBAC_ALLOW, 337 Policies: map[string]*v3rbacpb.Policy{ 338 "product-viewer": { 339 Permissions: []*v3rbacpb.Permission{ 340 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 341 }, 342 Principals: []*v3rbacpb.Principal{ 343 { 344 Identifier: &v3rbacpb.Principal_AndIds{AndIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{ 345 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 346 {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ 347 Ids: []*v3rbacpb.Principal{ 348 {Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/books"}}}}}}, 349 {Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/cars"}}}}}}, 350 }, 351 }}}, 352 }}}, 353 }, 354 }, 355 }, 356 }, 357 }, 358 }, 359 }, 360 // Certain Headers tests the construction of a chained engine with a 361 // policy that allows any downstream to send an HTTP request with 362 // certain headers. 363 { 364 name: "CertainHeaders", 365 policies: []*v3rbacpb.RBAC{ 366 { 367 Policies: map[string]*v3rbacpb.Policy{ 368 "certain-headers": { 369 Permissions: []*v3rbacpb.Permission{ 370 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 371 }, 372 Principals: []*v3rbacpb.Principal{ 373 { 374 Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{ 375 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 376 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "GET"}}}}}, 377 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_RangeMatch{RangeMatch: &v3typepb.Int64Range{ 378 Start: 0, 379 End: 64, 380 }}}}}, 381 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PresentMatch{PresentMatch: true}}}}, 382 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "GET"}}}}, 383 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "GET"}}}}, 384 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ContainsMatch{ContainsMatch: "GET"}}}}, 385 }}}, 386 }, 387 }, 388 }, 389 }, 390 }, 391 }, 392 }, 393 { 394 name: "LogAction", 395 policies: []*v3rbacpb.RBAC{ 396 { 397 Action: v3rbacpb.RBAC_LOG, 398 Policies: map[string]*v3rbacpb.Policy{ 399 "anyone": { 400 Permissions: []*v3rbacpb.Permission{ 401 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 402 }, 403 Principals: []*v3rbacpb.Principal{ 404 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 405 }, 406 }, 407 }, 408 }, 409 }, 410 wantErr: true, 411 }, 412 { 413 name: "ActionNotSpecified", 414 policies: []*v3rbacpb.RBAC{ 415 { 416 Policies: map[string]*v3rbacpb.Policy{ 417 "anyone": { 418 Permissions: []*v3rbacpb.Permission{ 419 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 420 }, 421 Principals: []*v3rbacpb.Principal{ 422 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 423 }, 424 }, 425 }, 426 }, 427 }, 428 }, 429 } 430 for _, test := range tests { 431 t.Run(test.name, func(t *testing.T) { 432 if _, err := NewChainEngine(test.policies); (err != nil) != test.wantErr { 433 t.Fatalf("NewChainEngine(%+v) returned err: %v, wantErr: %v", test.policies, err, test.wantErr) 434 } 435 }) 436 } 437 } 438 439 // TestChainEngine tests the chain of RBAC Engines by configuring the chain of 440 // engines in a certain way in different scenarios. After configuring the chain 441 // of engines in a certain way, this test pings the chain of engines with 442 // different types of data representing incoming RPC's (piped into a context), 443 // and verifies that it works as expected. 444 func (s) TestChainEngine(t *testing.T) { 445 defer func(gc func(ctx context.Context) net.Conn) { 446 getConnection = gc 447 }(getConnection) 448 tests := []struct { 449 name string 450 rbacConfigs []*v3rbacpb.RBAC 451 rbacQueries []struct { 452 rpcData *rpcData 453 wantStatusCode codes.Code 454 } 455 }{ 456 // SuccessCaseAnyMatch tests a single RBAC Engine instantiated with 457 // a config with a policy with any rules for both permissions and 458 // principals, meaning that any data about incoming RPC's that the RBAC 459 // Engine is queried with should match that policy. 460 { 461 name: "SuccessCaseAnyMatch", 462 rbacConfigs: []*v3rbacpb.RBAC{ 463 { 464 Policies: map[string]*v3rbacpb.Policy{ 465 "anyone": { 466 Permissions: []*v3rbacpb.Permission{ 467 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 468 }, 469 Principals: []*v3rbacpb.Principal{ 470 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 471 }, 472 }, 473 }, 474 }, 475 }, 476 rbacQueries: []struct { 477 rpcData *rpcData 478 wantStatusCode codes.Code 479 }{ 480 { 481 rpcData: &rpcData{ 482 fullMethod: "some method", 483 peerInfo: &peer.Peer{ 484 Addr: &addr{ipAddress: "0.0.0.0"}, 485 }, 486 }, 487 wantStatusCode: codes.OK, 488 }, 489 }, 490 }, 491 // SuccessCaseSimplePolicy is a test that tests a single policy 492 // that only allows an rpc to proceed if the rpc is calling with a certain 493 // path. 494 { 495 name: "SuccessCaseSimplePolicy", 496 rbacConfigs: []*v3rbacpb.RBAC{ 497 { 498 Policies: map[string]*v3rbacpb.Policy{ 499 "localhost-fan": { 500 Permissions: []*v3rbacpb.Permission{ 501 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 502 }, 503 Principals: []*v3rbacpb.Principal{ 504 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 505 }, 506 }, 507 }, 508 }, 509 }, 510 rbacQueries: []struct { 511 rpcData *rpcData 512 wantStatusCode codes.Code 513 }{ 514 // This RPC should match with the local host fan policy. Thus, 515 // this RPC should be allowed to proceed. 516 { 517 rpcData: &rpcData{ 518 fullMethod: "localhost-fan-page", 519 peerInfo: &peer.Peer{ 520 Addr: &addr{ipAddress: "0.0.0.0"}, 521 }, 522 }, 523 wantStatusCode: codes.OK, 524 }, 525 526 // This RPC shouldn't match with the local host fan policy. Thus, 527 // this rpc shouldn't be allowed to proceed. 528 { 529 rpcData: &rpcData{ 530 peerInfo: &peer.Peer{ 531 Addr: &addr{ipAddress: "0.0.0.0"}, 532 }, 533 }, 534 wantStatusCode: codes.PermissionDenied, 535 }, 536 }, 537 }, 538 // SuccessCaseEnvoyExample is a test based on the example provided 539 // in the EnvoyProxy docs. The RBAC Config contains two policies, 540 // service admin and product viewer, that provides an example of a real 541 // RBAC Config that might be configured for a given for a given backend 542 // service. 543 { 544 name: "SuccessCaseEnvoyExample", 545 rbacConfigs: []*v3rbacpb.RBAC{ 546 { 547 Policies: map[string]*v3rbacpb.Policy{ 548 "service-admin": { 549 Permissions: []*v3rbacpb.Permission{ 550 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 551 }, 552 Principals: []*v3rbacpb.Principal{ 553 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/admin"}}}}}, 554 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/superuser"}}}}}, 555 }, 556 }, 557 "product-viewer": { 558 Permissions: []*v3rbacpb.Permission{ 559 { 560 Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ 561 Rules: []*v3rbacpb.Permission{ 562 {Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 563 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}}, 564 }, 565 }, 566 }, 567 }, 568 }, 569 Principals: []*v3rbacpb.Principal{ 570 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 571 }, 572 }, 573 }, 574 }, 575 }, 576 rbacQueries: []struct { 577 rpcData *rpcData 578 wantStatusCode codes.Code 579 }{ 580 // This incoming RPC Call should match with the service admin 581 // policy. 582 { 583 rpcData: &rpcData{ 584 fullMethod: "some method", 585 peerInfo: &peer.Peer{ 586 Addr: &addr{ipAddress: "0.0.0.0"}, 587 AuthInfo: credentials.TLSInfo{ 588 State: tls.ConnectionState{ 589 PeerCertificates: []*x509.Certificate{ 590 { 591 URIs: []*url.URL{ 592 { 593 Host: "cluster.local", 594 Path: "/ns/default/sa/admin", 595 }, 596 }, 597 }, 598 }, 599 }, 600 }, 601 }, 602 }, 603 wantStatusCode: codes.OK, 604 }, 605 // These incoming RPC calls should not match any policy. 606 { 607 rpcData: &rpcData{ 608 peerInfo: &peer.Peer{ 609 Addr: &addr{ipAddress: "0.0.0.0"}, 610 }, 611 }, 612 wantStatusCode: codes.PermissionDenied, 613 }, 614 { 615 rpcData: &rpcData{ 616 fullMethod: "get-product-list", 617 peerInfo: &peer.Peer{ 618 Addr: &addr{ipAddress: "0.0.0.0"}, 619 }, 620 }, 621 wantStatusCode: codes.PermissionDenied, 622 }, 623 { 624 rpcData: &rpcData{ 625 peerInfo: &peer.Peer{ 626 Addr: &addr{ipAddress: "0.0.0.0"}, 627 AuthInfo: credentials.TLSInfo{ 628 State: tls.ConnectionState{ 629 PeerCertificates: []*x509.Certificate{ 630 { 631 Subject: pkix.Name{ 632 CommonName: "localhost", 633 }, 634 }, 635 }, 636 }, 637 }, 638 }, 639 }, 640 wantStatusCode: codes.PermissionDenied, 641 }, 642 }, 643 }, 644 { 645 name: "NotMatcher", 646 rbacConfigs: []*v3rbacpb.RBAC{ 647 { 648 Policies: map[string]*v3rbacpb.Policy{ 649 "not-secret-content": { 650 Permissions: []*v3rbacpb.Permission{ 651 { 652 Rule: &v3rbacpb.Permission_NotRule{ 653 NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}}, 654 }, 655 }, 656 }, 657 Principals: []*v3rbacpb.Principal{ 658 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 659 }, 660 }, 661 }, 662 }, 663 }, 664 rbacQueries: []struct { 665 rpcData *rpcData 666 wantStatusCode codes.Code 667 }{ 668 // This incoming RPC Call should match with the not-secret-content policy. 669 { 670 rpcData: &rpcData{ 671 fullMethod: "/regular-content", 672 peerInfo: &peer.Peer{ 673 Addr: &addr{ipAddress: "0.0.0.0"}, 674 }, 675 }, 676 wantStatusCode: codes.OK, 677 }, 678 // This incoming RPC Call shouldn't match with the not-secret-content-policy. 679 { 680 rpcData: &rpcData{ 681 fullMethod: "/secret-content", 682 peerInfo: &peer.Peer{ 683 Addr: &addr{ipAddress: "0.0.0.0"}, 684 }, 685 }, 686 wantStatusCode: codes.PermissionDenied, 687 }, 688 }, 689 }, 690 { 691 name: "DirectRemoteIpMatcher", 692 rbacConfigs: []*v3rbacpb.RBAC{ 693 { 694 Policies: map[string]*v3rbacpb.Policy{ 695 "certain-direct-remote-ip": { 696 Permissions: []*v3rbacpb.Permission{ 697 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 698 }, 699 Principals: []*v3rbacpb.Principal{ 700 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 701 }, 702 }, 703 }, 704 }, 705 }, 706 rbacQueries: []struct { 707 rpcData *rpcData 708 wantStatusCode codes.Code 709 }{ 710 // This incoming RPC Call should match with the certain-direct-remote-ip policy. 711 { 712 rpcData: &rpcData{ 713 peerInfo: &peer.Peer{ 714 Addr: &addr{ipAddress: "0.0.0.0"}, 715 }, 716 }, 717 wantStatusCode: codes.OK, 718 }, 719 // This incoming RPC Call shouldn't match with the certain-direct-remote-ip policy. 720 { 721 rpcData: &rpcData{ 722 peerInfo: &peer.Peer{ 723 Addr: &addr{ipAddress: "10.0.0.0"}, 724 }, 725 }, 726 wantStatusCode: codes.PermissionDenied, 727 }, 728 }, 729 }, 730 // This test tests a RBAC policy configured with a remote-ip policy. 731 // This should be logically equivalent to configuring a Engine with a 732 // direct-remote-ip policy, as per A41 - "allow equating RBAC's 733 // direct_remote_ip and remote_ip." 734 { 735 name: "RemoteIpMatcher", 736 rbacConfigs: []*v3rbacpb.RBAC{ 737 { 738 Policies: map[string]*v3rbacpb.Policy{ 739 "certain-remote-ip": { 740 Permissions: []*v3rbacpb.Permission{ 741 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 742 }, 743 Principals: []*v3rbacpb.Principal{ 744 {Identifier: &v3rbacpb.Principal_RemoteIp{RemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 745 }, 746 }, 747 }, 748 }, 749 }, 750 rbacQueries: []struct { 751 rpcData *rpcData 752 wantStatusCode codes.Code 753 }{ 754 // This incoming RPC Call should match with the certain-remote-ip policy. 755 { 756 rpcData: &rpcData{ 757 peerInfo: &peer.Peer{ 758 Addr: &addr{ipAddress: "0.0.0.0"}, 759 }, 760 }, 761 wantStatusCode: codes.OK, 762 }, 763 // This incoming RPC Call shouldn't match with the certain-remote-ip policy. 764 { 765 rpcData: &rpcData{ 766 peerInfo: &peer.Peer{ 767 Addr: &addr{ipAddress: "10.0.0.0"}, 768 }, 769 }, 770 wantStatusCode: codes.PermissionDenied, 771 }, 772 }, 773 }, 774 { 775 name: "DestinationIpMatcher", 776 rbacConfigs: []*v3rbacpb.RBAC{ 777 { 778 Policies: map[string]*v3rbacpb.Policy{ 779 "certain-destination-ip": { 780 Permissions: []*v3rbacpb.Permission{ 781 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 782 }, 783 Principals: []*v3rbacpb.Principal{ 784 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 785 }, 786 }, 787 }, 788 }, 789 }, 790 rbacQueries: []struct { 791 rpcData *rpcData 792 wantStatusCode codes.Code 793 }{ 794 // This incoming RPC Call shouldn't match with the 795 // certain-destination-ip policy, as the test listens on local 796 // host. 797 { 798 rpcData: &rpcData{ 799 peerInfo: &peer.Peer{ 800 Addr: &addr{ipAddress: "10.0.0.0"}, 801 }, 802 }, 803 wantStatusCode: codes.PermissionDenied, 804 }, 805 }, 806 }, 807 // AllowAndDenyPolicy tests a policy with an allow (on path) and 808 // deny (on port) policy chained together. This represents how a user 809 // configured interceptor would use this, and also is a potential 810 // configuration for a dynamic xds interceptor. 811 { 812 name: "AllowAndDenyPolicy", 813 rbacConfigs: []*v3rbacpb.RBAC{ 814 { 815 Policies: map[string]*v3rbacpb.Policy{ 816 "certain-source-ip": { 817 Permissions: []*v3rbacpb.Permission{ 818 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 819 }, 820 Principals: []*v3rbacpb.Principal{ 821 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 822 }, 823 }, 824 }, 825 Action: v3rbacpb.RBAC_ALLOW, 826 }, 827 { 828 Policies: map[string]*v3rbacpb.Policy{ 829 "localhost-fan": { 830 Permissions: []*v3rbacpb.Permission{ 831 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 832 }, 833 Principals: []*v3rbacpb.Principal{ 834 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 835 }, 836 }, 837 }, 838 Action: v3rbacpb.RBAC_DENY, 839 }, 840 }, 841 rbacQueries: []struct { 842 rpcData *rpcData 843 wantStatusCode codes.Code 844 }{ 845 // This RPC should match with the allow policy, and shouldn't 846 // match with the deny and thus should be allowed to proceed. 847 { 848 rpcData: &rpcData{ 849 peerInfo: &peer.Peer{ 850 Addr: &addr{ipAddress: "0.0.0.0"}, 851 }, 852 }, 853 wantStatusCode: codes.OK, 854 }, 855 // This RPC should match with both the allow policy and deny policy 856 // and thus shouldn't be allowed to proceed as matched with deny. 857 { 858 rpcData: &rpcData{ 859 fullMethod: "localhost-fan-page", 860 peerInfo: &peer.Peer{ 861 Addr: &addr{ipAddress: "0.0.0.0"}, 862 }, 863 }, 864 wantStatusCode: codes.PermissionDenied, 865 }, 866 // This RPC shouldn't match with either policy, and thus 867 // shouldn't be allowed to proceed as didn't match with allow. 868 { 869 rpcData: &rpcData{ 870 peerInfo: &peer.Peer{ 871 Addr: &addr{ipAddress: "10.0.0.0"}, 872 }, 873 }, 874 wantStatusCode: codes.PermissionDenied, 875 }, 876 // This RPC shouldn't match with allow, match with deny, and 877 // thus shouldn't be allowed to proceed. 878 { 879 rpcData: &rpcData{ 880 fullMethod: "localhost-fan-page", 881 peerInfo: &peer.Peer{ 882 Addr: &addr{ipAddress: "10.0.0.0"}, 883 }, 884 }, 885 wantStatusCode: codes.PermissionDenied, 886 }, 887 }, 888 }, 889 // This test tests that when there are no SANs or Subject's 890 // distinguished name in incoming RPC's, that authenticated matchers 891 // match against the empty string. 892 { 893 name: "default-matching-no-credentials", 894 rbacConfigs: []*v3rbacpb.RBAC{ 895 { 896 Policies: map[string]*v3rbacpb.Policy{ 897 "service-admin": { 898 Permissions: []*v3rbacpb.Permission{ 899 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 900 }, 901 Principals: []*v3rbacpb.Principal{ 902 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: ""}}}}}, 903 }, 904 }, 905 }, 906 }, 907 }, 908 rbacQueries: []struct { 909 rpcData *rpcData 910 wantStatusCode codes.Code 911 }{ 912 // This incoming RPC Call should match with the service admin 913 // policy. No authentication info is provided, so the 914 // authenticated matcher should match to the string matcher on 915 // the empty string, matching to the service-admin policy. 916 { 917 rpcData: &rpcData{ 918 fullMethod: "some method", 919 peerInfo: &peer.Peer{ 920 Addr: &addr{ipAddress: "0.0.0.0"}, 921 AuthInfo: credentials.TLSInfo{ 922 State: tls.ConnectionState{ 923 PeerCertificates: []*x509.Certificate{ 924 { 925 URIs: []*url.URL{ 926 { 927 Host: "cluster.local", 928 Path: "/ns/default/sa/admin", 929 }, 930 }, 931 }, 932 }, 933 }, 934 }, 935 }, 936 }, 937 wantStatusCode: codes.OK, 938 }, 939 }, 940 }, 941 } 942 943 for _, test := range tests { 944 t.Run(test.name, func(t *testing.T) { 945 // Instantiate the chainedRBACEngine with different configurations that are 946 // interesting to test and to query. 947 cre, err := NewChainEngine(test.rbacConfigs) 948 if err != nil { 949 t.Fatalf("Error constructing RBAC Engine: %v", err) 950 } 951 // Query the created chain of RBAC Engines with different args to see 952 // if the chain of RBAC Engines configured as such works as intended. 953 for _, data := range test.rbacQueries { 954 func() { 955 // Construct the context with three data points that have enough 956 // information to represent incoming RPC's. This will be how a 957 // user uses this API. A user will have to put MD, PeerInfo, and 958 // the connection the RPC is sent on in the context. 959 ctx := metadata.NewIncomingContext(context.Background(), data.rpcData.md) 960 961 // Make a TCP connection with a certain destination port. The 962 // address/port of this connection will be used to populate the 963 // destination ip/port in RPCData struct. This represents what 964 // the user of ChainEngine will have to place into 965 // context, as this is only way to get destination ip and port. 966 lis, err := net.Listen("tcp", "localhost:0") 967 if err != nil { 968 t.Fatalf("Error listening: %v", err) 969 } 970 defer lis.Close() 971 connCh := make(chan net.Conn, 1) 972 go func() { 973 conn, err := lis.Accept() 974 if err != nil { 975 t.Errorf("Error accepting connection: %v", err) 976 return 977 } 978 connCh <- conn 979 }() 980 _, err = net.Dial("tcp", lis.Addr().String()) 981 if err != nil { 982 t.Fatalf("Error dialing: %v", err) 983 } 984 conn := <-connCh 985 defer conn.Close() 986 getConnection = func(context.Context) net.Conn { 987 return conn 988 } 989 ctx = peer.NewContext(ctx, data.rpcData.peerInfo) 990 stream := &ServerTransportStreamWithMethod{ 991 method: data.rpcData.fullMethod, 992 } 993 994 ctx = grpc.NewContextWithServerTransportStream(ctx, stream) 995 err = cre.IsAuthorized(ctx) 996 if gotCode := status.Code(err); gotCode != data.wantStatusCode { 997 t.Fatalf("IsAuthorized(%+v, %+v) returned (%+v), want(%+v)", ctx, data.rpcData.fullMethod, gotCode, data.wantStatusCode) 998 } 999 }() 1000 } 1001 }) 1002 } 1003 } 1004 1005 type ServerTransportStreamWithMethod struct { 1006 method string 1007 } 1008 1009 func (sts *ServerTransportStreamWithMethod) Method() string { 1010 return sts.method 1011 } 1012 1013 func (sts *ServerTransportStreamWithMethod) SetHeader(md metadata.MD) error { 1014 return nil 1015 } 1016 1017 func (sts *ServerTransportStreamWithMethod) SendHeader(md metadata.MD) error { 1018 return nil 1019 } 1020 1021 func (sts *ServerTransportStreamWithMethod) SetTrailer(md metadata.MD) error { 1022 return nil 1023 }