gitee.com/zhaochuninhefei/gmgo@v0.0.31-0.20240209061119-069254a02979/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/zhaochuninhefei/gmgo/x509" 27 28 tls "gitee.com/zhaochuninhefei/gmgo/gmtls" 29 30 v3corepb "gitee.com/zhaochuninhefei/gmgo/go-control-plane/envoy/config/core/v3" 31 v3rbacpb "gitee.com/zhaochuninhefei/gmgo/go-control-plane/envoy/config/rbac/v3" 32 v3routepb "gitee.com/zhaochuninhefei/gmgo/go-control-plane/envoy/config/route/v3" 33 v3matcherpb "gitee.com/zhaochuninhefei/gmgo/go-control-plane/envoy/type/matcher/v3" 34 v3typepb "gitee.com/zhaochuninhefei/gmgo/go-control-plane/envoy/type/v3" 35 "gitee.com/zhaochuninhefei/gmgo/grpc" 36 "gitee.com/zhaochuninhefei/gmgo/grpc/codes" 37 "gitee.com/zhaochuninhefei/gmgo/grpc/credentials" 38 "gitee.com/zhaochuninhefei/gmgo/grpc/internal/grpctest" 39 "gitee.com/zhaochuninhefei/gmgo/grpc/metadata" 40 "gitee.com/zhaochuninhefei/gmgo/grpc/peer" 41 "gitee.com/zhaochuninhefei/gmgo/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 // HeaderMatcher_ExactMatch已废弃,改为使用HeaderMatcher_StringMatch 197 //{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 198 {Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "GET"}}}}}}, 199 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}}, 200 {Rule: &v3rbacpb.Permission_OrRules{OrRules: &v3rbacpb.Permission_Set{ 201 Rules: []*v3rbacpb.Permission{ 202 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 80}}, 203 {Rule: &v3rbacpb.Permission_DestinationPort{DestinationPort: 443}}, 204 }, 205 }, 206 }, 207 }, 208 }, 209 }, 210 }, 211 }, 212 }, 213 Principals: []*v3rbacpb.Principal{ 214 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 215 }, 216 }, 217 }, 218 }, 219 }, 220 }, 221 { 222 name: "SourceIpMatcherSuccessSingular", 223 policies: []*v3rbacpb.RBAC{ 224 { 225 Action: v3rbacpb.RBAC_ALLOW, 226 Policies: map[string]*v3rbacpb.Policy{ 227 "certain-source-ip": { 228 Permissions: []*v3rbacpb.Permission{ 229 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 230 }, 231 Principals: []*v3rbacpb.Principal{ 232 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 233 }, 234 }, 235 }, 236 }, 237 }, 238 }, 239 { 240 name: "SourceIpMatcherFailureSingular", 241 policies: []*v3rbacpb.RBAC{ 242 { 243 Action: v3rbacpb.RBAC_ALLOW, 244 Policies: map[string]*v3rbacpb.Policy{ 245 "certain-source-ip": { 246 Permissions: []*v3rbacpb.Permission{ 247 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 248 }, 249 Principals: []*v3rbacpb.Principal{ 250 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 251 }, 252 }, 253 }, 254 }, 255 }, 256 wantErr: true, 257 }, 258 { 259 name: "DestinationIpMatcherSuccess", 260 policies: []*v3rbacpb.RBAC{ 261 { 262 Action: v3rbacpb.RBAC_ALLOW, 263 Policies: map[string]*v3rbacpb.Policy{ 264 "certain-destination-ip": { 265 Permissions: []*v3rbacpb.Permission{ 266 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 267 }, 268 Principals: []*v3rbacpb.Principal{ 269 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 270 }, 271 }, 272 }, 273 }, 274 }, 275 }, 276 { 277 name: "DestinationIpMatcherFailure", 278 policies: []*v3rbacpb.RBAC{ 279 { 280 Action: v3rbacpb.RBAC_ALLOW, 281 Policies: map[string]*v3rbacpb.Policy{ 282 "certain-destination-ip": { 283 Permissions: []*v3rbacpb.Permission{ 284 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "not a correct address", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 285 }, 286 Principals: []*v3rbacpb.Principal{ 287 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 288 }, 289 }, 290 }, 291 }, 292 }, 293 wantErr: true, 294 }, 295 { 296 name: "MatcherToNotPolicy", 297 policies: []*v3rbacpb.RBAC{ 298 { 299 Action: v3rbacpb.RBAC_ALLOW, 300 Policies: map[string]*v3rbacpb.Policy{ 301 "not-secret-content": { 302 Permissions: []*v3rbacpb.Permission{ 303 {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"}}}}}}}}, 304 }, 305 Principals: []*v3rbacpb.Principal{ 306 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 307 }, 308 }, 309 }, 310 }, 311 }, 312 }, 313 { 314 name: "MatcherToNotPrinicipal", 315 policies: []*v3rbacpb.RBAC{ 316 { 317 Action: v3rbacpb.RBAC_ALLOW, 318 Policies: map[string]*v3rbacpb.Policy{ 319 "not-from-certain-ip": { 320 Permissions: []*v3rbacpb.Permission{ 321 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 322 }, 323 Principals: []*v3rbacpb.Principal{ 324 {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)}}}}}}, 325 }, 326 }, 327 }, 328 }, 329 }, 330 }, 331 // PrinicpalProductViewer tests the construction of a chained engine 332 // with a policy that allows any downstream to send a GET request on a 333 // certain path. 334 { 335 name: "PrincipalProductViewer", 336 policies: []*v3rbacpb.RBAC{ 337 { 338 Action: v3rbacpb.RBAC_ALLOW, 339 Policies: map[string]*v3rbacpb.Policy{ 340 "product-viewer": { 341 Permissions: []*v3rbacpb.Permission{ 342 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 343 }, 344 Principals: []*v3rbacpb.Principal{ 345 { 346 Identifier: &v3rbacpb.Principal_AndIds{AndIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{ 347 // HeaderMatcher_ExactMatch已废弃,改为使用HeaderMatcher_StringMatch 348 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 349 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "GET"}}}}}}, 350 {Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{ 351 Ids: []*v3rbacpb.Principal{ 352 {Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/books"}}}}}}, 353 {Identifier: &v3rbacpb.Principal_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/cars"}}}}}}, 354 }, 355 }}}, 356 }}}, 357 }, 358 }, 359 }, 360 }, 361 }, 362 }, 363 }, 364 // Certain Headers tests the construction of a chained engine with a 365 // policy that allows any downstream to send an HTTP request with 366 // certain headers. 367 { 368 name: "CertainHeaders", 369 policies: []*v3rbacpb.RBAC{ 370 { 371 Policies: map[string]*v3rbacpb.Policy{ 372 "certain-headers": { 373 Permissions: []*v3rbacpb.Permission{ 374 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 375 }, 376 Principals: []*v3rbacpb.Principal{ 377 { 378 Identifier: &v3rbacpb.Principal_OrIds{OrIds: &v3rbacpb.Principal_Set{Ids: []*v3rbacpb.Principal{ 379 // HeaderMatcher_ExactMatch已废弃,改为使用HeaderMatcher_StringMatch 380 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 381 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "GET"}}}}}}, 382 // HeaderMatcher_SafeRegexMatch已废弃,改为使用HeaderMatcher_StringMatch 383 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SafeRegexMatch{SafeRegexMatch: &v3matcherpb.RegexMatcher{Regex: "GET"}}}}}, 384 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{SafeRegex: &v3matcherpb.RegexMatcher{Regex: "GET"}}}}}}}, 385 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_RangeMatch{RangeMatch: &v3typepb.Int64Range{ 386 Start: 0, 387 End: 64, 388 }}}}}, 389 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PresentMatch{PresentMatch: true}}}}, 390 // HeaderMatcher_PrefixMatch已废弃,改为使用HeaderMatcher_StringMatch 391 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_PrefixMatch{PrefixMatch: "GET"}}}}, 392 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "GET"}}}}}}, 393 // HeaderMatcher_SuffixMatch已废弃,改为使用HeaderMatcher_StringMatch 394 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_SuffixMatch{SuffixMatch: "GET"}}}}, 395 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "GET"}}}}}}, 396 // HeaderMatcher_ContainsMatch已废弃,改为使用HeaderMatcher_StringMatch 397 //{Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ContainsMatch{ContainsMatch: "GET"}}}}, 398 {Identifier: &v3rbacpb.Principal_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: "GET"}}}}}}, 399 }}}, 400 }, 401 }, 402 }, 403 }, 404 }, 405 }, 406 }, 407 { 408 name: "LogAction", 409 policies: []*v3rbacpb.RBAC{ 410 { 411 Action: v3rbacpb.RBAC_LOG, 412 Policies: map[string]*v3rbacpb.Policy{ 413 "anyone": { 414 Permissions: []*v3rbacpb.Permission{ 415 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 416 }, 417 Principals: []*v3rbacpb.Principal{ 418 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 419 }, 420 }, 421 }, 422 }, 423 }, 424 wantErr: true, 425 }, 426 { 427 name: "ActionNotSpecified", 428 policies: []*v3rbacpb.RBAC{ 429 { 430 Policies: map[string]*v3rbacpb.Policy{ 431 "anyone": { 432 Permissions: []*v3rbacpb.Permission{ 433 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 434 }, 435 Principals: []*v3rbacpb.Principal{ 436 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 437 }, 438 }, 439 }, 440 }, 441 }, 442 }, 443 } 444 for _, test := range tests { 445 t.Run(test.name, func(t *testing.T) { 446 if _, err := NewChainEngine(test.policies); (err != nil) != test.wantErr { 447 t.Fatalf("NewChainEngine(%+v) returned err: %v, wantErr: %v", test.policies, err, test.wantErr) 448 } 449 }) 450 } 451 } 452 453 // TestChainEngine tests the chain of RBAC Engines by configuring the chain of 454 // engines in a certain way in different scenarios. After configuring the chain 455 // of engines in a certain way, this test pings the chain of engines with 456 // different types of data representing incoming RPC's (piped into a context), 457 // and verifies that it works as expected. 458 func (s) TestChainEngine(t *testing.T) { 459 defer func(gc func(ctx context.Context) net.Conn) { 460 getConnection = gc 461 }(getConnection) 462 tests := []struct { 463 name string 464 rbacConfigs []*v3rbacpb.RBAC 465 rbacQueries []struct { 466 rpcData *rpcData 467 wantStatusCode codes.Code 468 } 469 }{ 470 // SuccessCaseAnyMatch tests a single RBAC Engine instantiated with 471 // a config with a policy with any rules for both permissions and 472 // principals, meaning that any data about incoming RPC's that the RBAC 473 // Engine is queried with should match that policy. 474 { 475 name: "SuccessCaseAnyMatch", 476 rbacConfigs: []*v3rbacpb.RBAC{ 477 { 478 Policies: map[string]*v3rbacpb.Policy{ 479 "anyone": { 480 Permissions: []*v3rbacpb.Permission{ 481 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 482 }, 483 Principals: []*v3rbacpb.Principal{ 484 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 485 }, 486 }, 487 }, 488 }, 489 }, 490 rbacQueries: []struct { 491 rpcData *rpcData 492 wantStatusCode codes.Code 493 }{ 494 { 495 rpcData: &rpcData{ 496 fullMethod: "some method", 497 peerInfo: &peer.Peer{ 498 Addr: &addr{ipAddress: "0.0.0.0"}, 499 }, 500 }, 501 wantStatusCode: codes.OK, 502 }, 503 }, 504 }, 505 // SuccessCaseSimplePolicy is a test that tests a single policy 506 // that only allows an rpc to proceed if the rpc is calling with a certain 507 // path. 508 { 509 name: "SuccessCaseSimplePolicy", 510 rbacConfigs: []*v3rbacpb.RBAC{ 511 { 512 Policies: map[string]*v3rbacpb.Policy{ 513 "localhost-fan": { 514 Permissions: []*v3rbacpb.Permission{ 515 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 516 }, 517 Principals: []*v3rbacpb.Principal{ 518 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 519 }, 520 }, 521 }, 522 }, 523 }, 524 rbacQueries: []struct { 525 rpcData *rpcData 526 wantStatusCode codes.Code 527 }{ 528 // This RPC should match with the local host fan policy. Thus, 529 // this RPC should be allowed to proceed. 530 { 531 rpcData: &rpcData{ 532 fullMethod: "localhost-fan-page", 533 peerInfo: &peer.Peer{ 534 Addr: &addr{ipAddress: "0.0.0.0"}, 535 }, 536 }, 537 wantStatusCode: codes.OK, 538 }, 539 540 // This RPC shouldn't match with the local host fan policy. Thus, 541 // this rpc shouldn't be allowed to proceed. 542 { 543 rpcData: &rpcData{ 544 peerInfo: &peer.Peer{ 545 Addr: &addr{ipAddress: "0.0.0.0"}, 546 }, 547 }, 548 wantStatusCode: codes.PermissionDenied, 549 }, 550 }, 551 }, 552 // SuccessCaseEnvoyExample is a test based on the example provided 553 // in the EnvoyProxy docs. The RBAC Config contains two policies, 554 // service admin and product viewer, that provides an example of a real 555 // RBAC Config that might be configured for a given for a given backend 556 // service. 557 { 558 name: "SuccessCaseEnvoyExample", 559 rbacConfigs: []*v3rbacpb.RBAC{ 560 { 561 Policies: map[string]*v3rbacpb.Policy{ 562 "service-admin": { 563 Permissions: []*v3rbacpb.Permission{ 564 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 565 }, 566 Principals: []*v3rbacpb.Principal{ 567 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/admin"}}}}}, 568 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "//cluster.local/ns/default/sa/superuser"}}}}}, 569 }, 570 }, 571 "product-viewer": { 572 Permissions: []*v3rbacpb.Permission{ 573 { 574 Rule: &v3rbacpb.Permission_AndRules{AndRules: &v3rbacpb.Permission_Set{ 575 Rules: []*v3rbacpb.Permission{ 576 // HeaderMatcher_ExactMatch已废弃,改为使用HeaderMatcher_StringMatch 577 //{Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_ExactMatch{ExactMatch: "GET"}}}}, 578 {Rule: &v3rbacpb.Permission_Header{Header: &v3routepb.HeaderMatcher{Name: ":method", HeaderMatchSpecifier: &v3routepb.HeaderMatcher_StringMatch{StringMatch: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "GET"}}}}}}, 579 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/products"}}}}}}, 580 }, 581 }, 582 }, 583 }, 584 }, 585 Principals: []*v3rbacpb.Principal{ 586 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 587 }, 588 }, 589 }, 590 }, 591 }, 592 rbacQueries: []struct { 593 rpcData *rpcData 594 wantStatusCode codes.Code 595 }{ 596 // This incoming RPC Call should match with the service admin 597 // policy. 598 { 599 rpcData: &rpcData{ 600 fullMethod: "some method", 601 peerInfo: &peer.Peer{ 602 Addr: &addr{ipAddress: "0.0.0.0"}, 603 AuthInfo: credentials.TLSInfo{ 604 State: tls.ConnectionState{ 605 PeerCertificates: []*x509.Certificate{ 606 { 607 URIs: []*url.URL{ 608 { 609 Host: "cluster.local", 610 Path: "/ns/default/sa/admin", 611 }, 612 }, 613 }, 614 }, 615 }, 616 }, 617 }, 618 }, 619 wantStatusCode: codes.OK, 620 }, 621 // These incoming RPC calls should not match any policy. 622 { 623 rpcData: &rpcData{ 624 peerInfo: &peer.Peer{ 625 Addr: &addr{ipAddress: "0.0.0.0"}, 626 }, 627 }, 628 wantStatusCode: codes.PermissionDenied, 629 }, 630 { 631 rpcData: &rpcData{ 632 fullMethod: "get-product-list", 633 peerInfo: &peer.Peer{ 634 Addr: &addr{ipAddress: "0.0.0.0"}, 635 }, 636 }, 637 wantStatusCode: codes.PermissionDenied, 638 }, 639 { 640 rpcData: &rpcData{ 641 peerInfo: &peer.Peer{ 642 Addr: &addr{ipAddress: "0.0.0.0"}, 643 AuthInfo: credentials.TLSInfo{ 644 State: tls.ConnectionState{ 645 PeerCertificates: []*x509.Certificate{ 646 { 647 Subject: pkix.Name{ 648 CommonName: "localhost", 649 }, 650 }, 651 }, 652 }, 653 }, 654 }, 655 }, 656 wantStatusCode: codes.PermissionDenied, 657 }, 658 }, 659 }, 660 { 661 name: "NotMatcher", 662 rbacConfigs: []*v3rbacpb.RBAC{ 663 { 664 Policies: map[string]*v3rbacpb.Policy{ 665 "not-secret-content": { 666 Permissions: []*v3rbacpb.Permission{ 667 { 668 Rule: &v3rbacpb.Permission_NotRule{ 669 NotRule: &v3rbacpb.Permission{Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "/secret-content"}}}}}}, 670 }, 671 }, 672 }, 673 Principals: []*v3rbacpb.Principal{ 674 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 675 }, 676 }, 677 }, 678 }, 679 }, 680 rbacQueries: []struct { 681 rpcData *rpcData 682 wantStatusCode codes.Code 683 }{ 684 // This incoming RPC Call should match with the not-secret-content policy. 685 { 686 rpcData: &rpcData{ 687 fullMethod: "/regular-content", 688 peerInfo: &peer.Peer{ 689 Addr: &addr{ipAddress: "0.0.0.0"}, 690 }, 691 }, 692 wantStatusCode: codes.OK, 693 }, 694 // This incoming RPC Call shouldn't match with the not-secret-content-policy. 695 { 696 rpcData: &rpcData{ 697 fullMethod: "/secret-content", 698 peerInfo: &peer.Peer{ 699 Addr: &addr{ipAddress: "0.0.0.0"}, 700 }, 701 }, 702 wantStatusCode: codes.PermissionDenied, 703 }, 704 }, 705 }, 706 { 707 name: "DirectRemoteIpMatcher", 708 rbacConfigs: []*v3rbacpb.RBAC{ 709 { 710 Policies: map[string]*v3rbacpb.Policy{ 711 "certain-direct-remote-ip": { 712 Permissions: []*v3rbacpb.Permission{ 713 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 714 }, 715 Principals: []*v3rbacpb.Principal{ 716 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 717 }, 718 }, 719 }, 720 }, 721 }, 722 rbacQueries: []struct { 723 rpcData *rpcData 724 wantStatusCode codes.Code 725 }{ 726 // This incoming RPC Call should match with the certain-direct-remote-ip policy. 727 { 728 rpcData: &rpcData{ 729 peerInfo: &peer.Peer{ 730 Addr: &addr{ipAddress: "0.0.0.0"}, 731 }, 732 }, 733 wantStatusCode: codes.OK, 734 }, 735 // This incoming RPC Call shouldn't match with the certain-direct-remote-ip policy. 736 { 737 rpcData: &rpcData{ 738 peerInfo: &peer.Peer{ 739 Addr: &addr{ipAddress: "10.0.0.0"}, 740 }, 741 }, 742 wantStatusCode: codes.PermissionDenied, 743 }, 744 }, 745 }, 746 // This test tests a RBAC policy configured with a remote-ip policy. 747 // This should be logically equivalent to configuring a Engine with a 748 // direct-remote-ip policy, as per A41 - "allow equating RBAC's 749 // direct_remote_ip and remote_ip." 750 { 751 name: "RemoteIpMatcher", 752 rbacConfigs: []*v3rbacpb.RBAC{ 753 { 754 Policies: map[string]*v3rbacpb.Policy{ 755 "certain-remote-ip": { 756 Permissions: []*v3rbacpb.Permission{ 757 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 758 }, 759 Principals: []*v3rbacpb.Principal{ 760 {Identifier: &v3rbacpb.Principal_RemoteIp{RemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 761 }, 762 }, 763 }, 764 }, 765 }, 766 rbacQueries: []struct { 767 rpcData *rpcData 768 wantStatusCode codes.Code 769 }{ 770 // This incoming RPC Call should match with the certain-remote-ip policy. 771 { 772 rpcData: &rpcData{ 773 peerInfo: &peer.Peer{ 774 Addr: &addr{ipAddress: "0.0.0.0"}, 775 }, 776 }, 777 wantStatusCode: codes.OK, 778 }, 779 // This incoming RPC Call shouldn't match with the certain-remote-ip policy. 780 { 781 rpcData: &rpcData{ 782 peerInfo: &peer.Peer{ 783 Addr: &addr{ipAddress: "10.0.0.0"}, 784 }, 785 }, 786 wantStatusCode: codes.PermissionDenied, 787 }, 788 }, 789 }, 790 { 791 name: "DestinationIpMatcher", 792 rbacConfigs: []*v3rbacpb.RBAC{ 793 { 794 Policies: map[string]*v3rbacpb.Policy{ 795 "certain-destination-ip": { 796 Permissions: []*v3rbacpb.Permission{ 797 {Rule: &v3rbacpb.Permission_DestinationIp{DestinationIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 798 }, 799 Principals: []*v3rbacpb.Principal{ 800 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 801 }, 802 }, 803 }, 804 }, 805 }, 806 rbacQueries: []struct { 807 rpcData *rpcData 808 wantStatusCode codes.Code 809 }{ 810 // This incoming RPC Call shouldn't match with the 811 // certain-destination-ip policy, as the test listens on local 812 // host. 813 { 814 rpcData: &rpcData{ 815 peerInfo: &peer.Peer{ 816 Addr: &addr{ipAddress: "10.0.0.0"}, 817 }, 818 }, 819 wantStatusCode: codes.PermissionDenied, 820 }, 821 }, 822 }, 823 // AllowAndDenyPolicy tests a policy with an allow (on path) and 824 // deny (on port) policy chained together. This represents how a user 825 // configured interceptor would use this, and also is a potential 826 // configuration for a dynamic xds interceptor. 827 { 828 name: "AllowAndDenyPolicy", 829 rbacConfigs: []*v3rbacpb.RBAC{ 830 { 831 Policies: map[string]*v3rbacpb.Policy{ 832 "certain-source-ip": { 833 Permissions: []*v3rbacpb.Permission{ 834 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 835 }, 836 Principals: []*v3rbacpb.Principal{ 837 {Identifier: &v3rbacpb.Principal_DirectRemoteIp{DirectRemoteIp: &v3corepb.CidrRange{AddressPrefix: "0.0.0.0", PrefixLen: &wrapperspb.UInt32Value{Value: uint32(10)}}}}, 838 }, 839 }, 840 }, 841 Action: v3rbacpb.RBAC_ALLOW, 842 }, 843 { 844 Policies: map[string]*v3rbacpb.Policy{ 845 "localhost-fan": { 846 Permissions: []*v3rbacpb.Permission{ 847 {Rule: &v3rbacpb.Permission_UrlPath{UrlPath: &v3matcherpb.PathMatcher{Rule: &v3matcherpb.PathMatcher_Path{Path: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "localhost-fan-page"}}}}}}, 848 }, 849 Principals: []*v3rbacpb.Principal{ 850 {Identifier: &v3rbacpb.Principal_Any{Any: true}}, 851 }, 852 }, 853 }, 854 Action: v3rbacpb.RBAC_DENY, 855 }, 856 }, 857 rbacQueries: []struct { 858 rpcData *rpcData 859 wantStatusCode codes.Code 860 }{ 861 // This RPC should match with the allow policy, and shouldn't 862 // match with the deny and thus should be allowed to proceed. 863 { 864 rpcData: &rpcData{ 865 peerInfo: &peer.Peer{ 866 Addr: &addr{ipAddress: "0.0.0.0"}, 867 }, 868 }, 869 wantStatusCode: codes.OK, 870 }, 871 // This RPC should match with both the allow policy and deny policy 872 // and thus shouldn't be allowed to proceed as matched with deny. 873 { 874 rpcData: &rpcData{ 875 fullMethod: "localhost-fan-page", 876 peerInfo: &peer.Peer{ 877 Addr: &addr{ipAddress: "0.0.0.0"}, 878 }, 879 }, 880 wantStatusCode: codes.PermissionDenied, 881 }, 882 // This RPC shouldn't match with either policy, and thus 883 // shouldn't be allowed to proceed as didn't match with allow. 884 { 885 rpcData: &rpcData{ 886 peerInfo: &peer.Peer{ 887 Addr: &addr{ipAddress: "10.0.0.0"}, 888 }, 889 }, 890 wantStatusCode: codes.PermissionDenied, 891 }, 892 // This RPC shouldn't match with allow, match with deny, and 893 // thus shouldn't be allowed to proceed. 894 { 895 rpcData: &rpcData{ 896 fullMethod: "localhost-fan-page", 897 peerInfo: &peer.Peer{ 898 Addr: &addr{ipAddress: "10.0.0.0"}, 899 }, 900 }, 901 wantStatusCode: codes.PermissionDenied, 902 }, 903 }, 904 }, 905 // This test tests that when there are no SANs or Subject's 906 // distinguished name in incoming RPC's, that authenticated matchers 907 // match against the empty string. 908 { 909 name: "default-matching-no-credentials", 910 rbacConfigs: []*v3rbacpb.RBAC{ 911 { 912 Policies: map[string]*v3rbacpb.Policy{ 913 "service-admin": { 914 Permissions: []*v3rbacpb.Permission{ 915 {Rule: &v3rbacpb.Permission_Any{Any: true}}, 916 }, 917 Principals: []*v3rbacpb.Principal{ 918 {Identifier: &v3rbacpb.Principal_Authenticated_{Authenticated: &v3rbacpb.Principal_Authenticated{PrincipalName: &v3matcherpb.StringMatcher{MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: ""}}}}}, 919 }, 920 }, 921 }, 922 }, 923 }, 924 rbacQueries: []struct { 925 rpcData *rpcData 926 wantStatusCode codes.Code 927 }{ 928 // This incoming RPC Call should match with the service admin 929 // policy. No authentication info is provided, so the 930 // authenticated matcher should match to the string matcher on 931 // the empty string, matching to the service-admin policy. 932 { 933 rpcData: &rpcData{ 934 fullMethod: "some method", 935 peerInfo: &peer.Peer{ 936 Addr: &addr{ipAddress: "0.0.0.0"}, 937 AuthInfo: credentials.TLSInfo{ 938 State: tls.ConnectionState{ 939 PeerCertificates: []*x509.Certificate{ 940 { 941 URIs: []*url.URL{ 942 { 943 Host: "cluster.local", 944 Path: "/ns/default/sa/admin", 945 }, 946 }, 947 }, 948 }, 949 }, 950 }, 951 }, 952 }, 953 wantStatusCode: codes.OK, 954 }, 955 }, 956 }, 957 } 958 959 for _, test := range tests { 960 t.Run(test.name, func(t *testing.T) { 961 // Instantiate the chainedRBACEngine with different configurations that are 962 // interesting to test and to query. 963 cre, err := NewChainEngine(test.rbacConfigs) 964 if err != nil { 965 t.Fatalf("Error constructing RBAC Engine: %v", err) 966 } 967 // Query the created chain of RBAC Engines with different args to see 968 // if the chain of RBAC Engines configured as such works as intended. 969 for _, data := range test.rbacQueries { 970 func() { 971 // Construct the context with three data points that have enough 972 // information to represent incoming RPC's. This will be how a 973 // user uses this API. A user will have to put MD, PeerInfo, and 974 // the connection the RPC is sent on in the context. 975 ctx := metadata.NewIncomingContext(context.Background(), data.rpcData.md) 976 977 // Make a TCP connection with a certain destination port. The 978 // address/port of this connection will be used to populate the 979 // destination ip/port in RPCData struct. This represents what 980 // the user of ChainEngine will have to place into 981 // context, as this is only way to get destination ip and port. 982 lis, err := net.Listen("tcp", "localhost:0") 983 if err != nil { 984 t.Fatalf("Error listening: %v", err) 985 } 986 defer func(lis net.Listener) { 987 _ = lis.Close() 988 }(lis) 989 connCh := make(chan net.Conn, 1) 990 go func() { 991 conn, err := lis.Accept() 992 if err != nil { 993 t.Errorf("Error accepting connection: %v", err) 994 return 995 } 996 connCh <- conn 997 }() 998 _, err = net.Dial("tcp", lis.Addr().String()) 999 if err != nil { 1000 t.Fatalf("Error dialing: %v", err) 1001 } 1002 conn := <-connCh 1003 defer func(conn net.Conn) { 1004 _ = conn.Close() 1005 }(conn) 1006 getConnection = func(context.Context) net.Conn { 1007 return conn 1008 } 1009 ctx = peer.NewContext(ctx, data.rpcData.peerInfo) 1010 stream := &ServerTransportStreamWithMethod{ 1011 method: data.rpcData.fullMethod, 1012 } 1013 1014 ctx = grpc.NewContextWithServerTransportStream(ctx, stream) 1015 err = cre.IsAuthorized(ctx) 1016 if gotCode := status.Code(err); gotCode != data.wantStatusCode { 1017 t.Fatalf("IsAuthorized(%+v, %+v) returned (%+v), want(%+v)", ctx, data.rpcData.fullMethod, gotCode, data.wantStatusCode) 1018 } 1019 }() 1020 } 1021 }) 1022 } 1023 } 1024 1025 type ServerTransportStreamWithMethod struct { 1026 method string 1027 } 1028 1029 func (sts *ServerTransportStreamWithMethod) Method() string { 1030 return sts.method 1031 } 1032 1033 //goland:noinspection GoUnusedParameter 1034 func (sts *ServerTransportStreamWithMethod) SetHeader(md metadata.MD) error { 1035 return nil 1036 } 1037 1038 //goland:noinspection GoUnusedParameter 1039 func (sts *ServerTransportStreamWithMethod) SendHeader(md metadata.MD) error { 1040 return nil 1041 } 1042 1043 //goland:noinspection GoUnusedParameter 1044 func (sts *ServerTransportStreamWithMethod) SetTrailer(md metadata.MD) error { 1045 return nil 1046 }