/* Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package discovery import ( "context" "errors" "fmt" "reflect" "testing" "github.com/hyperledger/fabric-protos-go/peer" "github.com/golang/protobuf/proto" "github.com/hyperledger/fabric-protos-go/discovery" "github.com/hyperledger/fabric-protos-go/gossip" "github.com/hyperledger/fabric/gossip/api" gcommon "github.com/hyperledger/fabric/gossip/common" gdisc "github.com/hyperledger/fabric/gossip/discovery" "github.com/hyperledger/fabric/gossip/protoext" "github.com/hyperledger/fabric/protoutil" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestConfig(t *testing.T) { for _, trueOfFalse := range []bool{true, false} { conf := Config{ AuthCacheEnabled: trueOfFalse, AuthCachePurgeRetentionRatio: 0.5, AuthCacheMaxSize: 42, } service := NewService(conf, &mockSupport{}) require.Equal(t, trueOfFalse, service.auth.conf.enabled) require.Equal(t, 42, service.auth.conf.maxCacheSize) require.Equal(t, 0.5, service.auth.conf.purgeRetentionRatio) } } func TestService(t *testing.T) { conf := Config{ AuthCacheEnabled: true, } ctx := context.Background() req := &discovery.Request{ Authentication: &discovery.AuthInfo{ ClientIdentity: []byte{1, 2, 3}, }, Queries: []*discovery.Query{ { Channel: "noneExistentChannel", }, }, } mockSup := &mockSupport{} mockSup.On("ChannelExists", "noneExistentChannel").Return(false) mockSup.On("ChannelExists", "channelWithAccessDenied").Return(true) mockSup.On("ChannelExists", "channelWithAccessGranted").Return(true) mockSup.On("ChannelExists", "channelWithSomeProblem").Return(true) mockSup.On("EligibleForService", "channelWithAccessDenied", mock.Anything).Return(errors.New("foo")) mockSup.On("EligibleForService", "channelWithAccessGranted", mock.Anything).Return(nil) mockSup.On("EligibleForService", "channelWithSomeProblem", mock.Anything).Return(nil) ed1 := &discovery.EndorsementDescriptor{ Chaincode: "cc1", } ed2 := &discovery.EndorsementDescriptor{ Chaincode: "cc2", } ed3 := &discovery.EndorsementDescriptor{ Chaincode: "cc3", } mockSup.On("PeersForEndorsement", "unknownCC").Return(nil, errors.New("unknown chaincode")) mockSup.On("PeersForEndorsement", "cc1").Return(ed1, nil) mockSup.On("PeersForEndorsement", "cc2").Return(ed2, nil) mockSup.On("PeersForEndorsement", "cc3").Return(ed3, nil) service := NewService(conf, mockSup) // Scenario I: Channel does not exist resp, err := service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp) // Scenario II: Channel does not exist req.Queries[0].Channel = "channelWithAccessDenied" resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp) // Scenario III: Request with nil query req.Queries[0].Channel = "channelWithAccessGranted" req.Queries[0].Query = nil resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type") // Scenario IV: Request payload is invalid signedRequest := toSignedRequest(req) // Corrupt the payload by appending a zero byte at its end signedRequest.Payload = append(signedRequest.Payload, 0) resp, err = service.Discover(ctx, signedRequest) require.Nil(t, resp) require.Contains(t, err.Error(), "failed parsing request") // Scenario V: Request a CC query with no chaincodes at all req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{ {}, }, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "chaincode interest must contain at least one chaincode") // Scenario VI: Request a CC query with no interests at all req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{}, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "chaincode query must have at least one chaincode interest") // Scenario VII: Request a CC query with a chaincode name that is empty req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{{ Chaincodes: []*peer.ChaincodeCall{{ Name: "", }}, }}, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "chaincode name in interest cannot be empty") // Scenario VIII: Request with a CC query where one chaincode is unavailable req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{ { Chaincodes: []*peer.ChaincodeCall{{Name: "unknownCC"}}, }, { Chaincodes: []*peer.ChaincodeCall{{Name: "cc1"}}, }, }, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "failed constructing descriptor") require.Contains(t, resp.Results[0].GetError().Content, "unknownCC") // Scenario IX: Request with a CC query where all are available req.Queries[0].Query = &discovery.Query_CcQuery{ CcQuery: &discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{ { Chaincodes: []*peer.ChaincodeCall{{Name: "cc1"}}, }, { Chaincodes: []*peer.ChaincodeCall{{Name: "cc2"}}, }, { Chaincodes: []*peer.ChaincodeCall{{Name: "cc3"}}, }, }, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) expected := wrapResult(&discovery.ChaincodeQueryResult{ Content: []*discovery.EndorsementDescriptor{ed1, ed2, ed3}, }) require.Equal(t, expected, resp) // Scenario X: Request with a config query mockSup.On("Config", mock.Anything).Return(nil, errors.New("failed fetching config")).Once() req.Queries[0].Query = &discovery.Query_ConfigQuery{ ConfigQuery: &discovery.ConfigQuery{}, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "failed fetching config for channel channelWithAccessGranted") // Scenario XI: Request with a config query mockSup.On("Config", mock.Anything).Return(&discovery.ConfigResult{}, nil).Once() req.Queries[0].Query = &discovery.Query_ConfigQuery{ ConfigQuery: &discovery.ConfigQuery{}, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.NotNil(t, resp.Results[0].GetConfigResult()) // Scenario XII: Request with a membership query // Peers in membership view: { p0, p1, p2, p3} // Peers in channel view: {p1, p2, p4} // So that means that the returned peers for the channel should be the intersection // which is: {p1, p2}, but the returned peers for the local query should be // simply the membership view. peersInMembershipView := gdisc.Members{ aliveMsg(0), aliveMsg(1), aliveMsg(2), aliveMsg(3), } peersInChannelView := gdisc.Members{ stateInfoMsg(1), stateInfoMsg(2), stateInfoMsg(4), } // EligibleForService for an "empty" channel mockSup.On("EligibleForService", "", mock.Anything).Return(nil).Once() mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithAccessGranted")).Return(peersInChannelView, nil).Once() mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithSomeProblem")).Return(nil, errors.New("an error occurred")).Once() mockSup.On("Peers").Return(peersInMembershipView).Twice() mockSup.On("IdentityInfo").Return(api.PeerIdentitySet{ idInfo(0, "O2"), idInfo(1, "O2"), idInfo(2, "O3"), idInfo(3, "O3"), idInfo(4, "O3"), }).Twice() req.Queries = []*discovery.Query{ { Channel: "channelWithAccessGranted", Query: &discovery.Query_PeerQuery{ PeerQuery: &discovery.PeerMembershipQuery{}, }, }, { Query: &discovery.Query_LocalPeers{ LocalPeers: &discovery.LocalPeerQuery{}, }, }, { Channel: "channelWithSomeProblem", Query: &discovery.Query_PeerQuery{ PeerQuery: &discovery.PeerMembershipQuery{ Filter: &peer.ChaincodeInterest{}, }, }, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) expectedChannelResponse := &discovery.PeerMembershipResult{ PeersByOrg: map[string]*discovery.Peers{ "O2": { Peers: []*discovery.Peer{ { Identity: idInfo(1, "O2").Identity, StateInfo: stateInfoMsg(1).Envelope, MembershipInfo: aliveMsg(1).Envelope, }, }, }, "O3": { Peers: []*discovery.Peer{ { Identity: idInfo(2, "O3").Identity, StateInfo: stateInfoMsg(2).Envelope, MembershipInfo: aliveMsg(2).Envelope, }, }, }, }, } expectedLocalResponse := &discovery.PeerMembershipResult{ PeersByOrg: map[string]*discovery.Peers{ "O2": { Peers: []*discovery.Peer{ { Identity: idInfo(0, "O2").Identity, MembershipInfo: aliveMsg(0).Envelope, }, { Identity: idInfo(1, "O2").Identity, MembershipInfo: aliveMsg(1).Envelope, }, }, }, "O3": { Peers: []*discovery.Peer{ { Identity: idInfo(2, "O3").Identity, MembershipInfo: aliveMsg(2).Envelope, }, { Identity: idInfo(3, "O3").Identity, MembershipInfo: aliveMsg(3).Envelope, }, }, }, }, } require.Len(t, resp.Results, 3) require.Len(t, resp.Results[0].GetMembers().PeersByOrg, 2) require.Len(t, resp.Results[1].GetMembers().PeersByOrg, 2) require.Equal(t, "an error occurred", resp.Results[2].GetError().Content) for org, responsePeers := range resp.Results[0].GetMembers().PeersByOrg { err := peers(expectedChannelResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers)) require.NoError(t, err) } for org, responsePeers := range resp.Results[1].GetMembers().PeersByOrg { err := peers(expectedLocalResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers)) require.NoError(t, err) } // Scenario XIII: The client is eligible for channel queries but not for channel-less // since it's not an admin. It sends a query for a channel-less query but puts a channel in the query. // It should fail because channel-less query types cannot have a channel configured in them. req.Queries = []*discovery.Query{ { Channel: "channelWithAccessGranted", Query: &discovery.Query_LocalPeers{ LocalPeers: &discovery.LocalPeerQuery{}, }, }, } resp, err = service.Discover(ctx, toSignedRequest(req)) require.NoError(t, err) require.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type") } func TestValidateStructure(t *testing.T) { extractHash := func(ctx context.Context) []byte { return nil } // Scenarios I-V without TLS, scenarios VI onwards TLS // Scenario I: Nil request res, err := validateStructure(context.Background(), nil, false, extractHash) require.Nil(t, res) require.Equal(t, "nil request", err.Error()) // Scenario II: Malformed envelope res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: []byte{1, 2, 3}, }, false, extractHash) require.Nil(t, res) require.Contains(t, err.Error(), "failed parsing request") // Scenario III: Empty request res, err = validateStructure(context.Background(), &discovery.SignedRequest{}, false, extractHash) require.Nil(t, res) require.Equal(t, "access denied, no authentication info in request", err.Error()) // Scenario IV: request without a client identity req := &discovery.Request{ Authentication: &discovery.AuthInfo{}, } b, _ := proto.Marshal(req) res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: b, }, false, extractHash) require.Nil(t, res) require.Equal(t, "access denied, client identity wasn't supplied", err.Error()) // Scenario V: request with a client identity, should succeed because no TLS is used req = &discovery.Request{ Authentication: &discovery.AuthInfo{ ClientIdentity: []byte{1, 2, 3}, }, } b, _ = proto.Marshal(req) res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: b, }, false, extractHash) require.NoError(t, err) // Ensure returned request is as before serialization to bytes require.True(t, proto.Equal(req, res)) // Scenario VI: request with a client identity but with TLS enabled but client doesn't send a TLS cert req = &discovery.Request{ Authentication: &discovery.AuthInfo{ ClientIdentity: []byte{1, 2, 3}, }, } b, _ = proto.Marshal(req) res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: b, }, true, extractHash) require.Nil(t, res) require.Equal(t, "client didn't send a TLS certificate", err.Error()) // Scenario VII: request with a client identity and with TLS enabled but the TLS cert hash doesn't match // the computed one extractHash = func(ctx context.Context) []byte { return []byte{1, 2} } req = &discovery.Request{ Authentication: &discovery.AuthInfo{ ClientIdentity: []byte{1, 2, 3}, ClientTlsCertHash: []byte{1, 2, 3}, }, } b, _ = proto.Marshal(req) res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: b, }, true, extractHash) require.Nil(t, res) require.Equal(t, "client claimed TLS hash doesn't match computed TLS hash from gRPC stream", err.Error()) // Scenario VIII: request with a client identity and with TLS enabled and the TLS cert hash doesn't match // the computed one extractHash = func(ctx context.Context) []byte { return []byte{1, 2, 3} } req = &discovery.Request{ Authentication: &discovery.AuthInfo{ ClientIdentity: []byte{1, 2, 3}, ClientTlsCertHash: []byte{1, 2, 3}, }, } b, _ = proto.Marshal(req) res, err = validateStructure(context.Background(), &discovery.SignedRequest{ Payload: b, }, true, extractHash) require.NoError(t, err) require.NotNil(t, res) } func TestValidateCCQuery(t *testing.T) { err := validateCCQuery(&discovery.ChaincodeQuery{ Interests: []*peer.ChaincodeInterest{ nil, }, }) require.Equal(t, "chaincode interest is nil", err.Error()) } func wrapResult(responses ...interface{}) *discovery.Response { response := &discovery.Response{} for _, res := range responses { response.Results = append(response.Results, wrapQueryResult(res)) } return response } func wrapQueryResult(res interface{}) *discovery.QueryResult { if err, isErr := res.(*discovery.Error); isErr { return &discovery.QueryResult{ Result: &discovery.QueryResult_Error{ Error: err, }, } } if ccRes, isCCQuery := res.(*discovery.ChaincodeQueryResult); isCCQuery { return &discovery.QueryResult{ Result: &discovery.QueryResult_CcQueryRes{ CcQueryRes: ccRes, }, } } if membRes, isMembershipQuery := res.(*discovery.PeerMembershipResult); isMembershipQuery { return &discovery.QueryResult{ Result: &discovery.QueryResult_Members{ Members: membRes, }, } } if confRes, isConfQuery := res.(*discovery.ConfigResult); isConfQuery { return &discovery.QueryResult{ Result: &discovery.QueryResult_ConfigResult{ ConfigResult: confRes, }, } } panic(fmt.Sprint("invalid type:", reflect.TypeOf(res))) } func toSignedRequest(req *discovery.Request) *discovery.SignedRequest { b, _ := proto.Marshal(req) return &discovery.SignedRequest{ Payload: b, } } type mockSupport struct { mock.Mock } func (ms *mockSupport) ConfigSequence(channel string) uint64 { return 0 } func (ms *mockSupport) IdentityInfo() api.PeerIdentitySet { return ms.Called().Get(0).(api.PeerIdentitySet) } func (ms *mockSupport) ChannelExists(channel string) bool { return ms.Called(channel).Get(0).(bool) } func (ms *mockSupport) PeersOfChannel(channel gcommon.ChannelID) gdisc.Members { panic("not implemented") } func (ms *mockSupport) Peers() gdisc.Members { return ms.Called().Get(0).(gdisc.Members) } func (ms *mockSupport) PeersForEndorsement(channel gcommon.ChannelID, interest *peer.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) { cc := interest.Chaincodes[0].Name args := ms.Called(cc) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*discovery.EndorsementDescriptor), args.Error(1) } func (ms *mockSupport) PeersAuthorizedByCriteria(chainID gcommon.ChannelID, interest *peer.ChaincodeInterest) (gdisc.Members, error) { args := ms.Called(chainID) if args.Error(1) != nil { return nil, args.Error(1) } return args.Get(0).(gdisc.Members), args.Error(1) } func (*mockSupport) Chaincodes(id gcommon.ChannelID) []*gossip.Chaincode { panic("implement me") } func (ms *mockSupport) EligibleForService(channel string, data protoutil.SignedData) error { return ms.Called(channel, data).Error(0) } func (ms *mockSupport) Config(channel string) (*discovery.ConfigResult, error) { args := ms.Called(channel) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*discovery.ConfigResult), args.Error(1) } func idInfo(id int, org string) api.PeerIdentityInfo { endpoint := fmt.Sprintf("p%d", id) return api.PeerIdentityInfo{ PKIId: gcommon.PKIidType(endpoint), Organization: api.OrgIdentityType(org), Identity: api.PeerIdentityType(endpoint), } } func stateInfoMsg(id int) gdisc.NetworkMember { endpoint := fmt.Sprintf("p%d", id) pkiID := gcommon.PKIidType(endpoint) si := &gossip.StateInfo{ PkiId: pkiID, } gm := &gossip.GossipMessage{ Content: &gossip.GossipMessage_StateInfo{ StateInfo: si, }, } sm, _ := protoext.NoopSign(gm) return gdisc.NetworkMember{ PKIid: pkiID, Envelope: sm.Envelope, } } func aliveMsg(id int) gdisc.NetworkMember { endpoint := fmt.Sprintf("p%d", id) pkiID := gcommon.PKIidType(endpoint) am := &gossip.AliveMessage{ Membership: &gossip.Member{ PkiId: pkiID, Endpoint: endpoint, }, } gm := &gossip.GossipMessage{ Content: &gossip.GossipMessage_AliveMsg{ AliveMsg: am, }, } sm, _ := protoext.NoopSign(gm) return gdisc.NetworkMember{ PKIid: pkiID, Endpoint: endpoint, Envelope: sm.Envelope, } } type peers []*discovery.Peer func (ps peers) exists(p *discovery.Peer) error { var found bool for _, q := range ps { if reflect.DeepEqual(*p, *q) { found = true break } } if !found { return fmt.Errorf("%v wasn't found in %v", ps, p) } return nil } func (ps peers) compare(otherPeers peers) error { if len(ps) != len(otherPeers) { return fmt.Errorf("size mismatch: %d, %d", len(ps), len(otherPeers)) } for _, p := range otherPeers { if err := ps.exists(p); err != nil { return err } } for _, p := range ps { if err := otherPeers.exists(p); err != nil { return err } } return nil }