github.com/yimialmonte/fabric@v2.1.1+incompatible/discovery/service_test.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package discovery 8 9 import ( 10 "context" 11 "errors" 12 "fmt" 13 "reflect" 14 "testing" 15 16 "github.com/golang/protobuf/proto" 17 "github.com/hyperledger/fabric-protos-go/discovery" 18 "github.com/hyperledger/fabric-protos-go/gossip" 19 "github.com/hyperledger/fabric/gossip/api" 20 gcommon "github.com/hyperledger/fabric/gossip/common" 21 gdisc "github.com/hyperledger/fabric/gossip/discovery" 22 "github.com/hyperledger/fabric/gossip/protoext" 23 "github.com/hyperledger/fabric/protoutil" 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/mock" 26 ) 27 28 func TestConfig(t *testing.T) { 29 for _, trueOfFalse := range []bool{true, false} { 30 conf := Config{ 31 AuthCacheEnabled: trueOfFalse, 32 AuthCachePurgeRetentionRatio: 0.5, 33 AuthCacheMaxSize: 42, 34 } 35 service := NewService(conf, &mockSupport{}) 36 assert.Equal(t, trueOfFalse, service.auth.conf.enabled) 37 assert.Equal(t, 42, service.auth.conf.maxCacheSize) 38 assert.Equal(t, 0.5, service.auth.conf.purgeRetentionRatio) 39 } 40 } 41 42 func TestService(t *testing.T) { 43 conf := Config{ 44 AuthCacheEnabled: true, 45 } 46 ctx := context.Background() 47 req := &discovery.Request{ 48 Authentication: &discovery.AuthInfo{ 49 ClientIdentity: []byte{1, 2, 3}, 50 }, 51 Queries: []*discovery.Query{ 52 { 53 Channel: "noneExistentChannel", 54 }, 55 }, 56 } 57 mockSup := &mockSupport{} 58 mockSup.On("ChannelExists", "noneExistentChannel").Return(false) 59 mockSup.On("ChannelExists", "channelWithAccessDenied").Return(true) 60 mockSup.On("ChannelExists", "channelWithAccessGranted").Return(true) 61 mockSup.On("ChannelExists", "channelWithSomeProblem").Return(true) 62 mockSup.On("EligibleForService", "channelWithAccessDenied", mock.Anything).Return(errors.New("foo")) 63 mockSup.On("EligibleForService", "channelWithAccessGranted", mock.Anything).Return(nil) 64 mockSup.On("EligibleForService", "channelWithSomeProblem", mock.Anything).Return(nil) 65 ed1 := &discovery.EndorsementDescriptor{ 66 Chaincode: "cc1", 67 } 68 ed2 := &discovery.EndorsementDescriptor{ 69 Chaincode: "cc2", 70 } 71 ed3 := &discovery.EndorsementDescriptor{ 72 Chaincode: "cc3", 73 } 74 mockSup.On("PeersForEndorsement", "unknownCC").Return(nil, errors.New("unknown chaincode")) 75 mockSup.On("PeersForEndorsement", "cc1").Return(ed1, nil) 76 mockSup.On("PeersForEndorsement", "cc2").Return(ed2, nil) 77 mockSup.On("PeersForEndorsement", "cc3").Return(ed3, nil) 78 79 service := NewService(conf, mockSup) 80 81 // Scenario I: Channel does not exist 82 resp, err := service.Discover(ctx, toSignedRequest(req)) 83 assert.NoError(t, err) 84 assert.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp) 85 86 // Scenario II: Channel does not exist 87 req.Queries[0].Channel = "channelWithAccessDenied" 88 resp, err = service.Discover(ctx, toSignedRequest(req)) 89 assert.NoError(t, err) 90 assert.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp) 91 92 // Scenario III: Request with nil query 93 req.Queries[0].Channel = "channelWithAccessGranted" 94 req.Queries[0].Query = nil 95 resp, err = service.Discover(ctx, toSignedRequest(req)) 96 assert.NoError(t, err) 97 assert.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type") 98 99 // Scenario IV: Request payload is invalid 100 signedRequest := toSignedRequest(req) 101 // Corrupt the payload by appending a zero byte at its end 102 signedRequest.Payload = append(signedRequest.Payload, 0) 103 resp, err = service.Discover(ctx, signedRequest) 104 assert.Nil(t, resp) 105 assert.Contains(t, err.Error(), "failed parsing request") 106 107 // Scenario V: Request a CC query with no chaincodes at all 108 req.Queries[0].Query = &discovery.Query_CcQuery{ 109 CcQuery: &discovery.ChaincodeQuery{ 110 Interests: []*discovery.ChaincodeInterest{ 111 {}, 112 }, 113 }, 114 } 115 resp, err = service.Discover(ctx, toSignedRequest(req)) 116 assert.NoError(t, err) 117 assert.Contains(t, resp.Results[0].GetError().Content, "chaincode interest must contain at least one chaincode") 118 119 // Scenario VI: Request a CC query with no interests at all 120 req.Queries[0].Query = &discovery.Query_CcQuery{ 121 CcQuery: &discovery.ChaincodeQuery{ 122 Interests: []*discovery.ChaincodeInterest{}}, 123 } 124 resp, err = service.Discover(ctx, toSignedRequest(req)) 125 assert.NoError(t, err) 126 assert.Contains(t, resp.Results[0].GetError().Content, "chaincode query must have at least one chaincode interest") 127 128 // Scenario VII: Request a CC query with a chaincode name that is empty 129 req.Queries[0].Query = &discovery.Query_CcQuery{ 130 CcQuery: &discovery.ChaincodeQuery{ 131 Interests: []*discovery.ChaincodeInterest{{ 132 Chaincodes: []*discovery.ChaincodeCall{{ 133 Name: "", 134 }}, 135 }}}, 136 } 137 resp, err = service.Discover(ctx, toSignedRequest(req)) 138 assert.NoError(t, err) 139 assert.Contains(t, resp.Results[0].GetError().Content, "chaincode name in interest cannot be empty") 140 141 // Scenario VIII: Request with a CC query where one chaincode is unavailable 142 req.Queries[0].Query = &discovery.Query_CcQuery{ 143 CcQuery: &discovery.ChaincodeQuery{ 144 Interests: []*discovery.ChaincodeInterest{ 145 { 146 Chaincodes: []*discovery.ChaincodeCall{{Name: "unknownCC"}}, 147 }, 148 { 149 Chaincodes: []*discovery.ChaincodeCall{{Name: "cc1"}}, 150 }, 151 }, 152 }, 153 } 154 155 resp, err = service.Discover(ctx, toSignedRequest(req)) 156 assert.NoError(t, err) 157 assert.Contains(t, resp.Results[0].GetError().Content, "failed constructing descriptor") 158 assert.Contains(t, resp.Results[0].GetError().Content, "unknownCC") 159 160 // Scenario IX: Request with a CC query where all are available 161 req.Queries[0].Query = &discovery.Query_CcQuery{ 162 CcQuery: &discovery.ChaincodeQuery{ 163 Interests: []*discovery.ChaincodeInterest{ 164 { 165 Chaincodes: []*discovery.ChaincodeCall{{Name: "cc1"}}, 166 }, 167 { 168 Chaincodes: []*discovery.ChaincodeCall{{Name: "cc2"}}, 169 }, 170 { 171 Chaincodes: []*discovery.ChaincodeCall{{Name: "cc3"}}, 172 }, 173 }, 174 }, 175 } 176 resp, err = service.Discover(ctx, toSignedRequest(req)) 177 assert.NoError(t, err) 178 expected := wrapResult(&discovery.ChaincodeQueryResult{ 179 Content: []*discovery.EndorsementDescriptor{ed1, ed2, ed3}, 180 }) 181 assert.Equal(t, expected, resp) 182 183 // Scenario X: Request with a config query 184 mockSup.On("Config", mock.Anything).Return(nil, errors.New("failed fetching config")).Once() 185 req.Queries[0].Query = &discovery.Query_ConfigQuery{ 186 ConfigQuery: &discovery.ConfigQuery{}, 187 } 188 resp, err = service.Discover(ctx, toSignedRequest(req)) 189 assert.NoError(t, err) 190 assert.Contains(t, resp.Results[0].GetError().Content, "failed fetching config for channel channelWithAccessGranted") 191 192 // Scenario XI: Request with a config query 193 mockSup.On("Config", mock.Anything).Return(&discovery.ConfigResult{}, nil).Once() 194 req.Queries[0].Query = &discovery.Query_ConfigQuery{ 195 ConfigQuery: &discovery.ConfigQuery{}, 196 } 197 resp, err = service.Discover(ctx, toSignedRequest(req)) 198 assert.NoError(t, err) 199 assert.NotNil(t, resp.Results[0].GetConfigResult()) 200 201 // Scenario XII: Request with a membership query 202 // Peers in membership view: { p0, p1, p2, p3} 203 // Peers in channel view: {p1, p2, p4} 204 // So that means that the returned peers for the channel should be the intersection 205 // which is: {p1, p2}, but the returned peers for the local query should be 206 // simply the membership view. 207 peersInMembershipView := gdisc.Members{ 208 aliveMsg(0), aliveMsg(1), aliveMsg(2), aliveMsg(3), 209 } 210 peersInChannelView := gdisc.Members{ 211 stateInfoMsg(1), stateInfoMsg(2), stateInfoMsg(4), 212 } 213 // EligibleForService for an "empty" channel 214 mockSup.On("EligibleForService", "", mock.Anything).Return(nil).Once() 215 mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithAccessGranted")).Return(peersInChannelView, nil).Once() 216 mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithSomeProblem")).Return(nil, errors.New("an error occurred")).Once() 217 mockSup.On("Peers").Return(peersInMembershipView).Twice() 218 mockSup.On("IdentityInfo").Return(api.PeerIdentitySet{ 219 idInfo(0, "O2"), idInfo(1, "O2"), idInfo(2, "O3"), 220 idInfo(3, "O3"), idInfo(4, "O3"), 221 }).Twice() 222 223 req.Queries = []*discovery.Query{ 224 { 225 Channel: "channelWithAccessGranted", 226 Query: &discovery.Query_PeerQuery{ 227 PeerQuery: &discovery.PeerMembershipQuery{}, 228 }, 229 }, 230 { 231 Query: &discovery.Query_LocalPeers{ 232 LocalPeers: &discovery.LocalPeerQuery{}, 233 }, 234 }, 235 { 236 Channel: "channelWithSomeProblem", 237 Query: &discovery.Query_PeerQuery{ 238 PeerQuery: &discovery.PeerMembershipQuery{ 239 Filter: &discovery.ChaincodeInterest{}, 240 }, 241 }, 242 }, 243 } 244 resp, err = service.Discover(ctx, toSignedRequest(req)) 245 expectedChannelResponse := &discovery.PeerMembershipResult{ 246 PeersByOrg: map[string]*discovery.Peers{ 247 "O2": { 248 Peers: []*discovery.Peer{ 249 { 250 Identity: idInfo(1, "O2").Identity, 251 StateInfo: stateInfoMsg(1).Envelope, 252 MembershipInfo: aliveMsg(1).Envelope, 253 }, 254 }, 255 }, 256 "O3": { 257 Peers: []*discovery.Peer{ 258 { 259 Identity: idInfo(2, "O3").Identity, 260 StateInfo: stateInfoMsg(2).Envelope, 261 MembershipInfo: aliveMsg(2).Envelope, 262 }, 263 }, 264 }, 265 }, 266 } 267 expectedLocalResponse := &discovery.PeerMembershipResult{ 268 PeersByOrg: map[string]*discovery.Peers{ 269 "O2": { 270 Peers: []*discovery.Peer{ 271 { 272 Identity: idInfo(0, "O2").Identity, 273 MembershipInfo: aliveMsg(0).Envelope, 274 }, 275 { 276 Identity: idInfo(1, "O2").Identity, 277 MembershipInfo: aliveMsg(1).Envelope, 278 }, 279 }, 280 }, 281 "O3": { 282 Peers: []*discovery.Peer{ 283 { 284 Identity: idInfo(2, "O3").Identity, 285 MembershipInfo: aliveMsg(2).Envelope, 286 }, 287 { 288 Identity: idInfo(3, "O3").Identity, 289 MembershipInfo: aliveMsg(3).Envelope, 290 }, 291 }, 292 }, 293 }, 294 } 295 296 assert.Len(t, resp.Results, 3) 297 assert.Len(t, resp.Results[0].GetMembers().PeersByOrg, 2) 298 assert.Len(t, resp.Results[1].GetMembers().PeersByOrg, 2) 299 assert.Equal(t, "an error occurred", resp.Results[2].GetError().Content) 300 301 for org, responsePeers := range resp.Results[0].GetMembers().PeersByOrg { 302 err := peers(expectedChannelResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers)) 303 assert.NoError(t, err) 304 } 305 for org, responsePeers := range resp.Results[1].GetMembers().PeersByOrg { 306 err := peers(expectedLocalResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers)) 307 assert.NoError(t, err) 308 } 309 310 // Scenario XIII: The client is eligible for channel queries but not for channel-less 311 // since it's not an admin. It sends a query for a channel-less query but puts a channel in the query. 312 // It should fail because channel-less query types cannot have a channel configured in them. 313 req.Queries = []*discovery.Query{ 314 { 315 Channel: "channelWithAccessGranted", 316 Query: &discovery.Query_LocalPeers{ 317 LocalPeers: &discovery.LocalPeerQuery{}, 318 }, 319 }, 320 } 321 resp, err = service.Discover(ctx, toSignedRequest(req)) 322 assert.NoError(t, err) 323 assert.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type") 324 } 325 326 func TestValidateStructure(t *testing.T) { 327 extractHash := func(ctx context.Context) []byte { 328 return nil 329 } 330 // Scenarios I-V without TLS, scenarios VI onwards TLS 331 332 // Scenario I: Nil request 333 res, err := validateStructure(context.Background(), nil, false, extractHash) 334 assert.Nil(t, res) 335 assert.Equal(t, "nil request", err.Error()) 336 337 // Scenario II: Malformed envelope 338 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 339 Payload: []byte{1, 2, 3}, 340 }, false, extractHash) 341 assert.Nil(t, res) 342 assert.Contains(t, err.Error(), "failed parsing request") 343 344 // Scenario III: Empty request 345 res, err = validateStructure(context.Background(), &discovery.SignedRequest{}, false, extractHash) 346 assert.Nil(t, res) 347 assert.Equal(t, "access denied, no authentication info in request", err.Error()) 348 349 // Scenario IV: request without a client identity 350 req := &discovery.Request{ 351 Authentication: &discovery.AuthInfo{}, 352 } 353 b, _ := proto.Marshal(req) 354 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 355 Payload: b, 356 }, false, extractHash) 357 assert.Nil(t, res) 358 assert.Equal(t, "access denied, client identity wasn't supplied", err.Error()) 359 360 // Scenario V: request with a client identity, should succeed because no TLS is used 361 req = &discovery.Request{ 362 Authentication: &discovery.AuthInfo{ 363 ClientIdentity: []byte{1, 2, 3}, 364 }, 365 } 366 b, _ = proto.Marshal(req) 367 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 368 Payload: b, 369 }, false, extractHash) 370 assert.NoError(t, err) 371 // Ensure returned request is as before serialization to bytes 372 assert.True(t, proto.Equal(req, res)) 373 374 // Scenario VI: request with a client identity but with TLS enabled but client doesn't send a TLS cert 375 req = &discovery.Request{ 376 Authentication: &discovery.AuthInfo{ 377 ClientIdentity: []byte{1, 2, 3}, 378 }, 379 } 380 b, _ = proto.Marshal(req) 381 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 382 Payload: b, 383 }, true, extractHash) 384 assert.Nil(t, res) 385 assert.Equal(t, "client didn't send a TLS certificate", err.Error()) 386 387 // Scenario VII: request with a client identity and with TLS enabled but the TLS cert hash doesn't match 388 // the computed one 389 extractHash = func(ctx context.Context) []byte { 390 return []byte{1, 2} 391 } 392 req = &discovery.Request{ 393 Authentication: &discovery.AuthInfo{ 394 ClientIdentity: []byte{1, 2, 3}, 395 ClientTlsCertHash: []byte{1, 2, 3}, 396 }, 397 } 398 b, _ = proto.Marshal(req) 399 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 400 Payload: b, 401 }, true, extractHash) 402 assert.Nil(t, res) 403 assert.Equal(t, "client claimed TLS hash doesn't match computed TLS hash from gRPC stream", err.Error()) 404 405 // Scenario VIII: request with a client identity and with TLS enabled and the TLS cert hash doesn't match 406 // the computed one 407 extractHash = func(ctx context.Context) []byte { 408 return []byte{1, 2, 3} 409 } 410 req = &discovery.Request{ 411 Authentication: &discovery.AuthInfo{ 412 ClientIdentity: []byte{1, 2, 3}, 413 ClientTlsCertHash: []byte{1, 2, 3}, 414 }, 415 } 416 b, _ = proto.Marshal(req) 417 res, err = validateStructure(context.Background(), &discovery.SignedRequest{ 418 Payload: b, 419 }, true, extractHash) 420 } 421 422 func TestValidateCCQuery(t *testing.T) { 423 err := validateCCQuery(&discovery.ChaincodeQuery{ 424 Interests: []*discovery.ChaincodeInterest{ 425 nil, 426 }, 427 }) 428 assert.Equal(t, "chaincode interest is nil", err.Error()) 429 } 430 431 func wrapResult(responses ...interface{}) *discovery.Response { 432 response := &discovery.Response{} 433 for _, res := range responses { 434 response.Results = append(response.Results, wrapQueryResult(res)) 435 } 436 return response 437 } 438 439 func wrapQueryResult(res interface{}) *discovery.QueryResult { 440 if err, isErr := res.(*discovery.Error); isErr { 441 return &discovery.QueryResult{ 442 Result: &discovery.QueryResult_Error{ 443 Error: err, 444 }, 445 } 446 } 447 if ccRes, isCCQuery := res.(*discovery.ChaincodeQueryResult); isCCQuery { 448 return &discovery.QueryResult{ 449 Result: &discovery.QueryResult_CcQueryRes{ 450 CcQueryRes: ccRes, 451 }, 452 } 453 } 454 if membRes, isMembershipQuery := res.(*discovery.PeerMembershipResult); isMembershipQuery { 455 return &discovery.QueryResult{ 456 Result: &discovery.QueryResult_Members{ 457 Members: membRes, 458 }, 459 } 460 } 461 if confRes, isConfQuery := res.(*discovery.ConfigResult); isConfQuery { 462 return &discovery.QueryResult{ 463 Result: &discovery.QueryResult_ConfigResult{ 464 ConfigResult: confRes, 465 }, 466 } 467 } 468 panic(fmt.Sprint("invalid type:", reflect.TypeOf(res))) 469 } 470 471 func toSignedRequest(req *discovery.Request) *discovery.SignedRequest { 472 b, _ := proto.Marshal(req) 473 return &discovery.SignedRequest{ 474 Payload: b, 475 } 476 } 477 478 type mockSupport struct { 479 mock.Mock 480 } 481 482 func (ms *mockSupport) ConfigSequence(channel string) uint64 { 483 return 0 484 } 485 486 func (ms *mockSupport) IdentityInfo() api.PeerIdentitySet { 487 return ms.Called().Get(0).(api.PeerIdentitySet) 488 } 489 490 func (ms *mockSupport) ChannelExists(channel string) bool { 491 return ms.Called(channel).Get(0).(bool) 492 } 493 494 func (ms *mockSupport) PeersOfChannel(channel gcommon.ChannelID) gdisc.Members { 495 panic("not implemented") 496 } 497 498 func (ms *mockSupport) Peers() gdisc.Members { 499 return ms.Called().Get(0).(gdisc.Members) 500 } 501 502 func (ms *mockSupport) PeersForEndorsement(channel gcommon.ChannelID, interest *discovery.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) { 503 cc := interest.Chaincodes[0].Name 504 args := ms.Called(cc) 505 if args.Get(0) == nil { 506 return nil, args.Error(1) 507 } 508 return args.Get(0).(*discovery.EndorsementDescriptor), args.Error(1) 509 } 510 511 func (ms *mockSupport) PeersAuthorizedByCriteria(chainID gcommon.ChannelID, interest *discovery.ChaincodeInterest) (gdisc.Members, error) { 512 args := ms.Called(chainID) 513 if args.Error(1) != nil { 514 return nil, args.Error(1) 515 } 516 return args.Get(0).(gdisc.Members), args.Error(1) 517 } 518 519 func (*mockSupport) Chaincodes(id gcommon.ChannelID) []*gossip.Chaincode { 520 panic("implement me") 521 } 522 523 func (ms *mockSupport) EligibleForService(channel string, data protoutil.SignedData) error { 524 return ms.Called(channel, data).Error(0) 525 } 526 527 func (ms *mockSupport) Config(channel string) (*discovery.ConfigResult, error) { 528 args := ms.Called(channel) 529 if args.Get(0) == nil { 530 return nil, args.Error(1) 531 } 532 return args.Get(0).(*discovery.ConfigResult), args.Error(1) 533 } 534 535 func idInfo(id int, org string) api.PeerIdentityInfo { 536 endpoint := fmt.Sprintf("p%d", id) 537 return api.PeerIdentityInfo{ 538 PKIId: gcommon.PKIidType(endpoint), 539 Organization: api.OrgIdentityType(org), 540 Identity: api.PeerIdentityType(endpoint), 541 } 542 } 543 544 func stateInfoMsg(id int) gdisc.NetworkMember { 545 endpoint := fmt.Sprintf("p%d", id) 546 pkiID := gcommon.PKIidType(endpoint) 547 si := &gossip.StateInfo{ 548 PkiId: pkiID, 549 } 550 gm := &gossip.GossipMessage{ 551 Content: &gossip.GossipMessage_StateInfo{ 552 StateInfo: si, 553 }, 554 } 555 sm, _ := protoext.NoopSign(gm) 556 return gdisc.NetworkMember{ 557 PKIid: pkiID, 558 Envelope: sm.Envelope, 559 } 560 } 561 562 func aliveMsg(id int) gdisc.NetworkMember { 563 endpoint := fmt.Sprintf("p%d", id) 564 pkiID := gcommon.PKIidType(endpoint) 565 am := &gossip.AliveMessage{ 566 Membership: &gossip.Member{ 567 PkiId: pkiID, 568 Endpoint: endpoint, 569 }, 570 } 571 gm := &gossip.GossipMessage{ 572 Content: &gossip.GossipMessage_AliveMsg{ 573 AliveMsg: am, 574 }, 575 } 576 sm, _ := protoext.NoopSign(gm) 577 return gdisc.NetworkMember{ 578 PKIid: pkiID, 579 Endpoint: endpoint, 580 Envelope: sm.Envelope, 581 } 582 } 583 584 type peers []*discovery.Peer 585 586 func (ps peers) exists(p *discovery.Peer) error { 587 var found bool 588 for _, q := range ps { 589 if reflect.DeepEqual(*p, *q) { 590 found = true 591 break 592 } 593 } 594 if !found { 595 return fmt.Errorf("%v wasn't found in %v", ps, p) 596 } 597 return nil 598 } 599 600 func (ps peers) compare(otherPeers peers) error { 601 if len(ps) != len(otherPeers) { 602 return fmt.Errorf("size mismatch: %d, %d", len(ps), len(otherPeers)) 603 } 604 605 for _, p := range otherPeers { 606 if err := ps.exists(p); err != nil { 607 return err 608 } 609 } 610 611 for _, p := range ps { 612 if err := otherPeers.exists(p); err != nil { 613 return err 614 } 615 } 616 return nil 617 }