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