Skip to content

Commit

Permalink
[FAB-17963] Ch.Part.API: Validate join block (#1393)
Browse files Browse the repository at this point in the history
* Introduce function for validating the join block received by channel
participation rest API. `ValidateJoinBlock` will determine whether the
block contains an application channel or system channel and does basic
validation on the structure of the join block.

Signed-off-by: Danny Cao <dcao@us.ibm.com>
  • Loading branch information
caod123 committed Jun 22, 2020
1 parent 68e363d commit 3bb28dd
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 17 deletions.
18 changes: 10 additions & 8 deletions orderer/common/channelparticipation/mocks/channel_management.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions orderer/common/channelparticipation/restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ type ChannelManagement interface {
ChannelInfo(channelID string) (types.ChannelInfo, error)

// JoinChannel instructs the orderer to create a channel and join it with the provided config block.
JoinChannel(channelID string, configBlock *cb.Block) (types.ChannelInfo, error)
JoinChannel(channelID string, configBlock *cb.Block, isAppChannel bool) (types.ChannelInfo, error)

// RemoveChannel instructs the orderer to remove a channel.
// Depending on the removeStorage parameter, the storage resources are either removed or archived.
Expand Down Expand Up @@ -163,7 +163,13 @@ func (h *HTTPHandler) serveJoin(resp http.ResponseWriter, req *http.Request) {
return
}

info, err := h.registrar.JoinChannel(channelID, block)
isAppChannel, err := ValidateJoinBlock(channelID, block)
if err != nil {
h.sendResponseJsonError(resp, http.StatusBadRequest, errors.Wrap(err, "invalid join block"))
return
}

info, err := h.registrar.JoinChannel(channelID, block, isAppChannel)
if err == nil {
info.URL = path.Join(URLBaseV1Channels, info.Name)
h.logger.Debugf("Successfully joined channel: %s", info)
Expand Down
25 changes: 21 additions & 4 deletions orderer/common/channelparticipation/restapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import (
"path"
"testing"

"github.com/hyperledger/fabric-protos-go/common"
"github.com/hyperledger/fabric/orderer/common/channelparticipation"
"github.com/hyperledger/fabric/orderer/common/channelparticipation/mocks"
"github.com/hyperledger/fabric/orderer/common/localconfig"
"github.com/hyperledger/fabric/orderer/common/types"
"github.com/hyperledger/fabric/protoutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -245,7 +247,7 @@ func TestHTTPHandler_ServeHTTP_Join(t *testing.T) {
fakeManager.JoinChannelReturns(info, nil)

resp := httptest.NewRecorder()
req := genJoinRequestFormData(t, []byte{})
req := genJoinRequestFormData(t, validBlockBytes("ch-id"))
h.ServeHTTP(resp, req)
assert.Equal(t, http.StatusCreated, resp.Result().StatusCode)
assert.Equal(t, "application/json", resp.Result().Header.Get("Content-Type"))
Expand All @@ -260,7 +262,7 @@ func TestHTTPHandler_ServeHTTP_Join(t *testing.T) {
fakeManager, h := setup(config, t)
fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrSystemChannelExists)
resp := httptest.NewRecorder()
req := genJoinRequestFormData(t, []byte{})
req := genJoinRequestFormData(t, validBlockBytes("ch-id"))
h.ServeHTTP(resp, req)
checkErrorResponse(t, http.StatusMethodNotAllowed, "cannot join: system channel exists", resp)
assert.Equal(t, "GET", resp.Result().Header.Get("Allow"))
Expand All @@ -270,7 +272,7 @@ func TestHTTPHandler_ServeHTTP_Join(t *testing.T) {
fakeManager, h := setup(config, t)
fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrChannelAlreadyExists)
resp := httptest.NewRecorder()
req := genJoinRequestFormData(t, []byte{})
req := genJoinRequestFormData(t, validBlockBytes("ch-id"))
h.ServeHTTP(resp, req)
checkErrorResponse(t, http.StatusMethodNotAllowed, "cannot join: channel already exists", resp)
assert.Equal(t, "GET, DELETE", resp.Result().Header.Get("Allow"))
Expand All @@ -280,7 +282,7 @@ func TestHTTPHandler_ServeHTTP_Join(t *testing.T) {
fakeManager, h := setup(config, t)
fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrAppChannelsAlreadyExists)
resp := httptest.NewRecorder()
req := genJoinRequestFormData(t, []byte{})
req := genJoinRequestFormData(t, validBlockBytes("ch-id"))
h.ServeHTTP(resp, req)
checkErrorResponse(t, http.StatusForbidden, "cannot join: application channels already exist", resp)
})
Expand All @@ -293,6 +295,14 @@ func TestHTTPHandler_ServeHTTP_Join(t *testing.T) {
checkErrorResponse(t, http.StatusBadRequest, "cannot unmarshal file part config-block into a block: proto: common.Block: illegal tag 0 (wire type 1)", resp)
})

t.Run("bad body - invalid join block", func(t *testing.T) {
_, h := setup(config, t)
resp := httptest.NewRecorder()
req := genJoinRequestFormData(t, []byte{})
h.ServeHTTP(resp, req)
checkErrorResponse(t, http.StatusBadRequest, "invalid join block: block is not a config block", resp)
})

t.Run("content type mismatch", func(t *testing.T) {
_, h := setup(config, t)
resp := httptest.NewRecorder()
Expand Down Expand Up @@ -532,3 +542,10 @@ func genJoinRequestFormData(t *testing.T, blockBytes []byte) *http.Request {

return req
}

func validBlockBytes(channelID string) []byte {
blockBytes := protoutil.MarshalOrPanic(blockWithGroups(map[string]*common.ConfigGroup{
"Application": {},
}, channelID))
return blockBytes
}
54 changes: 54 additions & 0 deletions orderer/common/channelparticipation/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package channelparticipation

import (
"errors"
"fmt"

cb "github.com/hyperledger/fabric-protos-go/common"
"github.com/hyperledger/fabric/bccsp/factory"
"github.com/hyperledger/fabric/common/channelconfig"
"github.com/hyperledger/fabric/protoutil"
)

// ValidateJoinBlock returns whether this block can be used as a join block for the channel participation API
// and whether it is an system channel if it contains consortiums, or otherwise
// and application channel if an application group exists.
func ValidateJoinBlock(channelID string, configBlock *cb.Block) (isAppChannel bool, err error) {
if !protoutil.IsConfigBlock(configBlock) {
return false, errors.New("block is not a config block")
}

envelope, err := protoutil.ExtractEnvelope(configBlock, 0)
if err != nil {
return false, err
}

cryptoProvider := factory.GetDefault()
bundle, err := channelconfig.NewBundleFromEnvelope(envelope, cryptoProvider)
if err != nil {
return false, err
}

// Check channel id from join block matches
if bundle.ConfigtxValidator().ChannelID() != channelID {
return false, fmt.Errorf("config block channelID [%s] does not match passed channelID [%s]",
bundle.ConfigtxValidator().ChannelID(), channelID)
}

// Check channel type
_, isSystemChannel := bundle.ConsortiumsConfig()
if !isSystemChannel {
_, isAppChannel = bundle.ApplicationConfig()
if !isAppChannel {
return false, errors.New("invalid config: must have at least one of application or consortiums")
}
}

return isAppChannel, err
}
166 changes: 166 additions & 0 deletions orderer/common/channelparticipation/validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package channelparticipation_test

import (
"errors"
"math"
"testing"

cb "github.com/hyperledger/fabric-protos-go/common"
"github.com/hyperledger/fabric/bccsp"
"github.com/hyperledger/fabric/orderer/common/channelparticipation"
"github.com/hyperledger/fabric/protoutil"
"github.com/stretchr/testify/assert"
)

func TestValidateJoinBlock(t *testing.T) {
tests := []struct {
testName string
channelID string
joinBlock *cb.Block
expectedIsAppChannel bool
expectedErr error
}{
{
testName: "Valid system channel join block",
channelID: "my-channel",
joinBlock: blockWithGroups(
map[string]*cb.ConfigGroup{
"Consortiums": {},
},
"my-channel",
),
expectedIsAppChannel: false,
expectedErr: nil,
},
{
testName: "Valid application channel join block",
channelID: "my-channel",
joinBlock: blockWithGroups(
map[string]*cb.ConfigGroup{
"Application": {},
},
"my-channel",
),
expectedIsAppChannel: true,
expectedErr: nil,
},
{
testName: "Join block not a config block",
channelID: "my-channel",
joinBlock: nonConfigBlock(),
expectedIsAppChannel: false,
expectedErr: errors.New("block is not a config block"),
},
{
testName: "ChannelID does not match join blocks",
channelID: "not-my-channel",
joinBlock: blockWithGroups(
map[string]*cb.ConfigGroup{
"Consortiums": {},
},
"my-channel",
),
expectedIsAppChannel: false,
expectedErr: errors.New("config block channelID [my-channel] does not match passed channelID [not-my-channel]"),
},
{
testName: "Invalid bundle",
channelID: "my-channel",
joinBlock: blockWithGroups(
map[string]*cb.ConfigGroup{
"InvalidGroup": {},
},
"my-channel",
),
expectedIsAppChannel: false,
expectedErr: nil,
},
{
testName: "Join block has no application or consortiums group",
channelID: "my-channel",
joinBlock: blockWithGroups(
map[string]*cb.ConfigGroup{},
"my-channel",
),
expectedIsAppChannel: false,
expectedErr: errors.New("invalid config: must have at least one of application or consortiums"),
},
}

for _, test := range tests {
t.Run(test.testName, func(t *testing.T) {
isAppChannel, err := channelparticipation.ValidateJoinBlock(test.channelID, test.joinBlock)
assert.Equal(t, isAppChannel, test.expectedIsAppChannel)
if test.expectedErr != nil {
assert.EqualError(t, err, test.expectedErr.Error())
}
})
}
}

func blockWithGroups(groups map[string]*cb.ConfigGroup, channelID string) *cb.Block {
return &cb.Block{
Data: &cb.BlockData{
Data: [][]byte{
protoutil.MarshalOrPanic(&cb.Envelope{
Payload: protoutil.MarshalOrPanic(&cb.Payload{
Data: protoutil.MarshalOrPanic(&cb.ConfigEnvelope{
Config: &cb.Config{
ChannelGroup: &cb.ConfigGroup{
Groups: groups,
Values: map[string]*cb.ConfigValue{
"HashingAlgorithm": {
Value: protoutil.MarshalOrPanic(&cb.HashingAlgorithm{
Name: bccsp.SHA256,
}),
},
"BlockDataHashingStructure": {
Value: protoutil.MarshalOrPanic(&cb.BlockDataHashingStructure{
Width: math.MaxUint32,
}),
},
"OrdererAddresses": {
Value: protoutil.MarshalOrPanic(&cb.OrdererAddresses{
Addresses: []string{"localhost"},
}),
},
},
},
},
}),
Header: &cb.Header{
ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
Type: int32(cb.HeaderType_CONFIG),
ChannelId: channelID,
}),
},
}),
}),
},
},
}
}

func nonConfigBlock() *cb.Block {
return &cb.Block{
Data: &cb.BlockData{
Data: [][]byte{
protoutil.MarshalOrPanic(&cb.Envelope{
Payload: protoutil.MarshalOrPanic(&cb.Payload{
Header: &cb.Header{
ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
Type: int32(cb.HeaderType_ENDORSER_TRANSACTION),
}),
},
}),
}),
},
},
}
}
2 changes: 1 addition & 1 deletion orderer/common/multichannel/registrar.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ func (r *Registrar) ChannelInfo(channelID string) (types.ChannelInfo, error) {
return info, nil
}

func (r *Registrar) JoinChannel(channelID string, configBlock *cb.Block) (types.ChannelInfo, error) {
func (r *Registrar) JoinChannel(channelID string, configBlock *cb.Block, isAppChannel bool) (types.ChannelInfo, error) {
r.lock.RLock()
defer r.lock.RUnlock()

Expand Down
Loading

0 comments on commit 3bb28dd

Please sign in to comment.