Skip to content

Commit bb2bd7d

Browse files
committed
[FAB-7715] Handle invalid couchdb index defs
Invalid CouchDB index definitions may be packaged into chaincode installation, causing runtime problems when the chaincode is installed/instantiated. This change - Adds metadata validation for golang and node.js chaincode packaging - Adds index JSON validation for files in META-INF/statedb/couchdb/indexes. - Ensures no metadata files are packaged from other META-INF directories. Change-Id: I6dc5f7cbb0ae48edfc29824813bf9f5ccaf17b1c Signed-off-by: David Enyeart <enyeart@us.ibm.com>
1 parent 261a4a6 commit bb2bd7d

File tree

12 files changed

+417
-6
lines changed

12 files changed

+417
-6
lines changed

core/chaincode/platforms/golang/platform.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
"github.com/hyperledger/fabric/common/metadata"
3434
"github.com/hyperledger/fabric/core/chaincode/platforms/util"
35+
ccmetadata "github.com/hyperledger/fabric/core/common/ccprovider/metadata"
3536
cutil "github.com/hyperledger/fabric/core/container/util"
3637
pb "github.com/hyperledger/fabric/protos/peer"
3738
"github.com/spf13/viper"
@@ -428,8 +429,8 @@ func (goPlatform *Platform) GetDeploymentPayload(spec *pb.ChaincodeSpec) ([]byte
428429
for _, file := range files {
429430

430431
// If the file is metadata rather than golang code, remove the leading go code path, for example:
431-
// file.Name: src/github.com/hyperledger/fabric/examples/chaincode/go/marbles02/META-INF/statedb/couchdb/indexes/indexOwner.json
432-
// tar file: META-INF/statedb/couchdb/indexes/indexOwner.json
432+
// original file.Name: src/github.com/hyperledger/fabric/examples/chaincode/go/marbles02/META-INF/statedb/couchdb/indexes/indexOwner.json
433+
// updated file.Name: META-INF/statedb/couchdb/indexes/indexOwner.json
433434
if file.IsMetadata {
434435

435436
// Ensure META-INF directory can be found, then grab the META-INF relative path to use for packaging
@@ -440,9 +441,25 @@ func (goPlatform *Platform) GetDeploymentPayload(spec *pb.ChaincodeSpec) ([]byte
440441
if err != nil {
441442
return nil, fmt.Errorf("Could not get relative path for META-INF directory %s. Error:%s", file.Name, err)
442443
}
444+
445+
// Split the filename itself from its path
446+
_, filename := filepath.Split(file.Name)
447+
448+
// Hidden files are not supported as metadata, therefore ignore them.
449+
// User often doesn't know that hidden files are there, and may not be able to delete them, therefore warn user rather than error out.
450+
if strings.HasPrefix(filename, ".") {
451+
logger.Warningf("Ignoring hidden file in metadata directory: %s", file.Name)
452+
continue
453+
}
454+
455+
// Validate metadata file for inclusion in tar
456+
// Validation is based on the passed metadata directory, e.g. META-INF/statedb/couchdb/indexes
457+
err = ccmetadata.ValidateMetadataFile(file.Path, filepath.Dir(file.Name))
458+
if err != nil {
459+
return nil, err
460+
}
443461
}
444462

445-
logger.Debug("Writing file to chaincode code package tarball:", file.Name)
446463
err = cutil.WriteFileToPackage(file.Path, file.Name, tw)
447464
if err != nil {
448465
return nil, fmt.Errorf("Error writing %s to tar: %s", file.Name, err)

core/chaincode/platforms/golang/platform_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,9 @@ func TestGetDeploymentPayload(t *testing.T) {
271271
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/examples/chaincode/go/map"}}, succ: true},
272272
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/examples/bad/go/map"}}, succ: false},
273273
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/test/chaincodes/BadImport"}}, succ: false},
274+
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/test/chaincodes/BadMetadataInvalidIndex"}}, succ: false},
275+
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/test/chaincodes/BadMetadataUnexpectedFolderContent"}}, succ: false},
276+
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/test/chaincodes/BadMetadataIgnoreHiddenFile"}}, succ: true},
274277
{spec: &pb.ChaincodeSpec{ChaincodeId: &pb.ChaincodeID{Name: "Test Chaincode", Path: "github.com/hyperledger/fabric/core/chaincode/platforms/golang/" + emptyDir}}, succ: false},
275278
}
276279

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package metadata
8+
9+
import (
10+
"encoding/json"
11+
"errors"
12+
"fmt"
13+
"io/ioutil"
14+
15+
"github.com/hyperledger/fabric/common/flogging"
16+
)
17+
18+
var logger = flogging.MustGetLogger("metadata")
19+
20+
// fileValidators are used as handlers to validate specific metadata directories
21+
type fileValidator func(srcPath string) error
22+
23+
// Currently, the only metadata expected and allowed is for META-INF/statedb/couchdb/indexes.
24+
var fileValidators = map[string]fileValidator{
25+
"META-INF/statedb/couchdb/indexes": couchdbIndexFileValidator,
26+
}
27+
28+
// UnhandledDirectoryError is returned for metadata files in unhandled directories
29+
type UnhandledDirectoryError struct {
30+
err string
31+
}
32+
33+
func (e *UnhandledDirectoryError) Error() string {
34+
return e.err
35+
}
36+
37+
// InvalidFileError is returned for invalid metadata files
38+
type InvalidFileError struct {
39+
err string
40+
}
41+
42+
func (e *InvalidFileError) Error() string {
43+
return e.err
44+
}
45+
46+
// ValidateMetadataFile checks that metadata files are valid
47+
// according to the validation rules of the metadata directory (metadataType)
48+
func ValidateMetadataFile(srcPath, metadataType string) error {
49+
// Get the validator handler for the metadata directory
50+
fileValidator, ok := fileValidators[metadataType]
51+
52+
// If there is no validator handler for metadata directory, return UnhandledDirectoryError
53+
if !ok {
54+
return &UnhandledDirectoryError{fmt.Sprintf("Metadata not supported in directory: %s", metadataType)}
55+
}
56+
57+
// If the file is not valid for the given metadata directory, return InvalidFileError
58+
err := fileValidator(srcPath)
59+
if err != nil {
60+
return &InvalidFileError{fmt.Sprintf("Metadata file [%s] failed validation: %s", srcPath, err)}
61+
}
62+
63+
// file is valid, return nil error
64+
return nil
65+
}
66+
67+
// couchdbIndexFileValidator implements fileValidator
68+
func couchdbIndexFileValidator(srcPath string) error {
69+
fileBytes, err := ioutil.ReadFile(srcPath)
70+
if err != nil {
71+
return err
72+
}
73+
74+
// if the content does not validate as JSON, return err to invalidate the file
75+
if !isJSON(string(fileBytes)) {
76+
return errors.New("File is not valid JSON")
77+
}
78+
79+
// TODO Additional validation to ensure the JSON represents a valid couchdb index definition
80+
81+
// file is a valid couchdb index definition, return nil error
82+
return nil
83+
}
84+
85+
// isJSON tests a string to determine if it can be parsed as valid JSON
86+
func isJSON(s string) bool {
87+
var js map[string]interface{}
88+
return json.Unmarshal([]byte(s), &js) == nil
89+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package metadata
8+
9+
import (
10+
"io/ioutil"
11+
"os"
12+
"path/filepath"
13+
"testing"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
var packageTestDir = filepath.Join(os.TempDir(), "ccmetadata-validator-test")
19+
20+
func TestGoodIndexJSON(t *testing.T) {
21+
testDir := filepath.Join(packageTestDir, "GoodIndexJSON")
22+
cleanupDir(testDir)
23+
defer cleanupDir(testDir)
24+
25+
filename := filepath.Join(testDir, "META-INF/statedb/couchdb/indexes", "myIndex.json")
26+
filebytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
27+
28+
err := writeToFile(filename, filebytes)
29+
assert.NoError(t, err, "Error writing to file")
30+
31+
err = ValidateMetadataFile(filename, "META-INF/statedb/couchdb/indexes")
32+
assert.NoError(t, err, "Error validating a good index")
33+
}
34+
35+
func TestBadIndexJSON(t *testing.T) {
36+
testDir := filepath.Join(packageTestDir, "BadIndexJSON")
37+
cleanupDir(testDir)
38+
defer cleanupDir(testDir)
39+
40+
filename := filepath.Join(testDir, "META-INF/statedb/couchdb/indexes", "myIndex.json")
41+
filebytes := []byte("invalid json")
42+
43+
err := writeToFile(filename, filebytes)
44+
assert.NoError(t, err, "Error writing to file")
45+
46+
err = ValidateMetadataFile(filename, "META-INF/statedb/couchdb/indexes")
47+
48+
assert.Error(t, err, "Should have received an InvalidFileError")
49+
50+
// Type assertion on InvalidFileError
51+
_, ok := err.(*InvalidFileError)
52+
assert.True(t, ok, "Should have received an InvalidFileError")
53+
54+
t.Log("SAMPLE ERROR STRING:", err.Error())
55+
}
56+
57+
func TestIndexWrongLocation(t *testing.T) {
58+
testDir := filepath.Join(packageTestDir, "IndexWrongLocation")
59+
cleanupDir(testDir)
60+
defer cleanupDir(testDir)
61+
62+
// place the index one directory too high
63+
filename := filepath.Join(testDir, "META-INF/statedb/couchdb", "myIndex.json")
64+
filebytes := []byte("invalid json")
65+
66+
err := writeToFile(filename, filebytes)
67+
assert.NoError(t, err, "Error writing to file")
68+
69+
err = ValidateMetadataFile(filename, "META-INF/statedb/couchdb")
70+
assert.Error(t, err, "Should have received an UnhandledDirectoryError")
71+
72+
// Type assertion on UnhandledDirectoryError
73+
_, ok := err.(*UnhandledDirectoryError)
74+
assert.True(t, ok, "Should have received an UnhandledDirectoryError")
75+
76+
t.Log("SAMPLE ERROR STRING:", err.Error())
77+
}
78+
79+
func TestInvalidMetadataType(t *testing.T) {
80+
testDir := filepath.Join(packageTestDir, "InvalidMetadataType")
81+
cleanupDir(testDir)
82+
defer cleanupDir(testDir)
83+
84+
filename := filepath.Join(testDir, "META-INF/statedb/couchdb/indexes", "myIndex.json")
85+
filebytes := []byte("invalid json")
86+
87+
err := writeToFile(filename, filebytes)
88+
assert.NoError(t, err, "Error writing to file")
89+
90+
err = ValidateMetadataFile(filename, "Invalid metadata type")
91+
assert.Error(t, err, "Should have received an UnhandledDirectoryError")
92+
93+
// Type assertion on UnhandledDirectoryError
94+
_, ok := err.(*UnhandledDirectoryError)
95+
assert.True(t, ok, "Should have received an UnhandledDirectoryError")
96+
}
97+
98+
func TestCantReadFile(t *testing.T) {
99+
testDir := filepath.Join(packageTestDir, "CantReadFile")
100+
cleanupDir(testDir)
101+
defer cleanupDir(testDir)
102+
103+
filename := filepath.Join(testDir, "META-INF/statedb/couchdb/indexes", "myIndex.json")
104+
105+
// Don't write the file - test for can't read file
106+
// err := writeToFile(filename, filebytes)
107+
// assert.NoError(t, err, "Error writing to file")
108+
109+
err := ValidateMetadataFile(filename, "META-INF/statedb/couchdb/indexes")
110+
assert.Error(t, err, "Should have received error reading file")
111+
}
112+
113+
func cleanupDir(dir string) error {
114+
// clean up any previous files
115+
err := os.RemoveAll(dir)
116+
if err != nil {
117+
return nil
118+
}
119+
return os.Mkdir(dir, os.ModePerm)
120+
}
121+
122+
func writeToFile(filename string, bytes []byte) error {
123+
dir := filepath.Dir(filename)
124+
err := os.MkdirAll(dir, os.ModePerm)
125+
if err != nil {
126+
return err
127+
}
128+
129+
return ioutil.WriteFile(filename, bytes, 0644)
130+
}

core/container/util/writer.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818

1919
"github.com/hyperledger/fabric/common/flogging"
20+
ccmetadata "github.com/hyperledger/fabric/core/common/ccprovider/metadata"
2021
"github.com/pkg/errors"
2122
)
2223

@@ -85,11 +86,30 @@ func WriteFolderToTarPackage(tw *tar.Writer, srcPath string, excludeDir string,
8586
}
8687

8788
var newPath string
89+
8890
// if file is metadata, keep the /META-INF directory, e.g: META-INF/statedb/couchdb/indexes/indexOwner.json
8991
// otherwise file is source code, put it in /src dir, e.g: src/marbles_chaincode.js
9092
if strings.HasPrefix(path, filepath.Join(rootDirectory, "META-INF")) {
9193
newPath = path[rootDirLen+1:]
92-
} else {
94+
95+
// Split the filename itself from its path
96+
_, filename := filepath.Split(newPath)
97+
98+
// Hidden files are not supported as metadata, therefore ignore them.
99+
// User often doesn't know that hidden files are there, and may not be able to delete them, therefore warn user rather than error out.
100+
if strings.HasPrefix(filename, ".") {
101+
vmLogger.Warningf("Ignoring hidden file in metadata directory: %s", newPath)
102+
return nil
103+
}
104+
105+
// Validate metadata file for inclusion in tar
106+
// Validation is based on the passed metadata directory, e.g. META-INF/statedb/couchdb/indexes
107+
err = ccmetadata.ValidateMetadataFile(path, filepath.Dir(newPath))
108+
if err != nil {
109+
return err
110+
}
111+
112+
} else { // file is not metadata, include in src
93113
newPath = fmt.Sprintf("src%s", path[rootDirLen:])
94114
}
95115

@@ -99,8 +119,6 @@ func WriteFolderToTarPackage(tw *tar.Writer, srcPath string, excludeDir string,
99119
}
100120
fileCount++
101121

102-
vmLogger.Debugf("Writing file %s to tar", newPath)
103-
104122
return nil
105123
}
106124

0 commit comments

Comments
 (0)