Skip to content

Commit 9a3b181

Browse files
author
Chris Elder
committed
[FAB-7779] CouchDB indexes for private data
Any couchdb indexes defined for a chaincode will be deployed in the chaincode state database. Indexes for collections of private data should be applied in the chaincode's collection state database rather than in the chaincode state database. Added support for HandleChaincodeDeploy event to common_storage_db. This will now process the event from chain code deploy. The move was necessary for handling the private data namespace mapping. Added new function to statecouchdb for processing index creation. Added new interface to statedb for IndexCapable. This allows the common_storage_db to detect that the state database will accept index creation events. Updated index validation to allow for indexes under collections directories. Change-Id: Icfda923c584d953ce5886023f7411bbddb59d595 Signed-off-by: Chris Elder <chris.elder@us.ibm.com>
1 parent 7400cc1 commit 9a3b181

File tree

13 files changed

+415
-172
lines changed

13 files changed

+415
-172
lines changed

common/ledger/testutil/test_util.go

+33
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package testutil
1818

1919
import (
20+
"archive/tar"
21+
"bytes"
2022
"crypto/rand"
2123
"encoding/json"
2224
"fmt"
@@ -169,3 +171,34 @@ func getCallerInfo() string {
169171
}
170172
return fmt.Sprintf("CallerInfo = [%s:%d]", file, line)
171173
}
174+
175+
// TarFileEntry is a structure for adding test index files to an tar
176+
type TarFileEntry struct {
177+
Name, Body string
178+
}
179+
180+
// CreateTarBytesForTest creates a tar byte array for unit testing
181+
func CreateTarBytesForTest(testFiles []*TarFileEntry) []byte {
182+
//Create a buffer for the tar file
183+
buffer := new(bytes.Buffer)
184+
tarWriter := tar.NewWriter(buffer)
185+
186+
for _, file := range testFiles {
187+
tarHeader := &tar.Header{
188+
Name: file.Name,
189+
Mode: 0600,
190+
Size: int64(len(file.Body)),
191+
}
192+
err := tarWriter.WriteHeader(tarHeader)
193+
if err != nil {
194+
return nil
195+
}
196+
_, err = tarWriter.Write([]byte(file.Body))
197+
if err != nil {
198+
return nil
199+
}
200+
}
201+
// Make sure to check the error on Close.
202+
tarWriter.Close()
203+
return buffer.Bytes()
204+
}

core/chaincode/platforms/ccmetadata/validators.go

+79-24
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,27 @@ import (
1111
"fmt"
1212
"path/filepath"
1313
"reflect"
14+
"regexp"
1415
"strings"
1516
)
1617

1718
// fileValidators are used as handlers to validate specific metadata directories
1819
type fileValidator func(fileName string, fileBytes []byte) error
1920

21+
const allowedCharsCollectionName = "[A-Za-z0-9_-]+"
22+
2023
// Currently, the only metadata expected and allowed is for META-INF/statedb/couchdb/indexes.
21-
var fileValidators = map[string]fileValidator{
22-
"META-INF/statedb/couchdb/indexes": couchdbIndexFileValidator,
24+
var fileValidators = map[*regexp.Regexp]fileValidator{
25+
regexp.MustCompile("^META-INF/statedb/couchdb/indexes/.*[.]json"): couchdbIndexFileValidator,
26+
regexp.MustCompile("^META-INF/statedb/couchdb/collections/" + allowedCharsCollectionName + "/indexes/.*[.]json"): couchdbIndexFileValidator,
2327
}
2428

29+
var collectionNameValid = regexp.MustCompile("^" + allowedCharsCollectionName)
30+
31+
var fileNameValid = regexp.MustCompile("^.*[.]json")
32+
33+
var validDatabases = []string{"couchdb"}
34+
2535
// UnhandledDirectoryError is returned for metadata files in unhandled directories
2636
type UnhandledDirectoryError struct {
2737
err string
@@ -31,15 +41,6 @@ func (e *UnhandledDirectoryError) Error() string {
3141
return e.err
3242
}
3343

34-
// BadExtensionError is returned for metadata files with extension other than .json
35-
type BadExtensionError struct {
36-
err string
37-
}
38-
39-
func (e *BadExtensionError) Error() string {
40-
return e.err
41-
}
42-
4344
// InvalidIndexContentError is returned for metadata files with invalid content
4445
type InvalidIndexContentError struct {
4546
err string
@@ -50,18 +51,18 @@ func (e *InvalidIndexContentError) Error() string {
5051
}
5152

5253
// ValidateMetadataFile checks that metadata files are valid
53-
// according to the validation rules of the metadata directory (metadataType)
54-
func ValidateMetadataFile(fileName string, fileBytes []byte, metadataType string) error {
54+
// according to the validation rules of the file's directory
55+
func ValidateMetadataFile(filePathName string, fileBytes []byte) error {
5556
// Get the validator handler for the metadata directory
56-
fileValidator, ok := fileValidators[metadataType]
57+
fileValidator := selectFileValidator(filePathName)
5758

5859
// If there is no validator handler for metadata directory, return UnhandledDirectoryError
59-
if !ok {
60-
return &UnhandledDirectoryError{fmt.Sprintf("Metadata not supported in directory: %s", metadataType)}
60+
if fileValidator == nil {
61+
return &UnhandledDirectoryError{buildMetadataFileErrorMessage(filePathName)}
6162
}
6263

63-
// If the file is not valid for the given metadata directory, return the corresponding error
64-
err := fileValidator(fileName, fileBytes)
64+
// If the file is not valid for the given directory-based validator, return the corresponding error
65+
err := fileValidator(filePathName, fileBytes)
6566
if err != nil {
6667
return err
6768
}
@@ -70,16 +71,70 @@ func ValidateMetadataFile(fileName string, fileBytes []byte, metadataType string
7071
return nil
7172
}
7273

73-
// couchdbIndexFileValidator implements fileValidator
74-
func couchdbIndexFileValidator(fileName string, fileBytes []byte) error {
74+
func buildMetadataFileErrorMessage(filePathName string) string {
7575

76-
ext := filepath.Ext(fileName)
76+
dir, filename := filepath.Split(filePathName)
7777

78-
// if the file does not have a .json extension, then return as error
79-
if ext != ".json" {
80-
return &BadExtensionError{fmt.Sprintf("Index metadata file [%s] does not have a .json extension", fileName)}
78+
if !strings.HasPrefix(filePathName, "META-INF/statedb") {
79+
return fmt.Sprintf("metadata file path must begin with META-INF/statedb, found: %s", dir)
80+
}
81+
directoryArray := strings.Split(filepath.Clean(dir), "/")
82+
// verify the minimum directory depth
83+
if len(directoryArray) < 4 {
84+
return fmt.Sprintf("metadata file path must include a database and index directory: %s", dir)
85+
}
86+
// validate the database type
87+
if !contains(validDatabases, directoryArray[2]) {
88+
return fmt.Sprintf("database name [%s] is not supported, valid options: %s", directoryArray[2], validDatabases)
89+
}
90+
// verify "indexes" is under the database name
91+
if len(directoryArray) == 4 && directoryArray[3] != "indexes" {
92+
return fmt.Sprintf("metadata file path does not have an indexes directory: %s", dir)
93+
}
94+
// if this is for collections, check the path length
95+
if len(directoryArray) != 6 {
96+
return fmt.Sprintf("metadata file path for collections must include a collections and index directory: %s", dir)
97+
}
98+
// verify "indexes" is under the collections and collection directories
99+
if directoryArray[3] != "collections" || directoryArray[5] != "indexes" {
100+
return fmt.Sprintf("metadata file path for collections must have a collections and indexes directory: %s", dir)
101+
}
102+
// validate the collection name
103+
if !collectionNameValid.MatchString(directoryArray[4]) {
104+
return fmt.Sprintf("collection name is not valid: %s", directoryArray[4])
81105
}
82106

107+
// validate the file name
108+
if !fileNameValid.MatchString(filename) {
109+
return fmt.Sprintf("artifact file name is not valid: %s", filename)
110+
}
111+
112+
return fmt.Sprintf("metadata file path or name is not supported: %s", dir)
113+
114+
}
115+
116+
func contains(validStrings []string, target string) bool {
117+
for _, str := range validStrings {
118+
if str == target {
119+
return true
120+
}
121+
}
122+
return false
123+
}
124+
125+
func selectFileValidator(filePathName string) fileValidator {
126+
for validateExp, fileValidator := range fileValidators {
127+
isValid := validateExp.MatchString(filePathName)
128+
if isValid {
129+
return fileValidator
130+
}
131+
}
132+
return nil
133+
}
134+
135+
// couchdbIndexFileValidator implements fileValidator
136+
func couchdbIndexFileValidator(fileName string, fileBytes []byte) error {
137+
83138
// if the content does not validate as JSON, return err to invalidate the file
84139
boolIsJSON, indexDefinition := isJSON(fileBytes)
85140
if !boolIsJSON {

core/chaincode/platforms/ccmetadata/validators_test.go

+80-18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0
77
package ccmetadata
88

99
import (
10+
"fmt"
1011
"io/ioutil"
1112
"os"
1213
"path/filepath"
@@ -22,11 +23,10 @@ func TestGoodIndexJSON(t *testing.T) {
2223
cleanupDir(testDir)
2324
defer cleanupDir(testDir)
2425

25-
fileName := "myIndex.json"
26+
fileName := "META-INF/statedb/couchdb/indexes/myIndex.json"
2627
fileBytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
27-
metadataType := "META-INF/statedb/couchdb/indexes"
2828

29-
err := ValidateMetadataFile(fileName, fileBytes, metadataType)
29+
err := ValidateMetadataFile(fileName, fileBytes)
3030
assert.NoError(t, err, "Error validating a good index")
3131
}
3232

@@ -35,11 +35,10 @@ func TestBadIndexJSON(t *testing.T) {
3535
cleanupDir(testDir)
3636
defer cleanupDir(testDir)
3737

38-
fileName := "myIndex.json"
38+
fileName := "META-INF/statedb/couchdb/indexes/myIndex.json"
3939
fileBytes := []byte("invalid json")
40-
metadataType := "META-INF/statedb/couchdb/indexes"
4140

42-
err := ValidateMetadataFile(fileName, fileBytes, metadataType)
41+
err := ValidateMetadataFile(fileName, fileBytes)
4342

4443
assert.Error(t, err, "Should have received an InvalidIndexContentError")
4544

@@ -55,12 +54,10 @@ func TestIndexWrongLocation(t *testing.T) {
5554
cleanupDir(testDir)
5655
defer cleanupDir(testDir)
5756

58-
fileName := "myIndex.json"
57+
fileName := "META-INF/statedb/couchdb/myIndex.json"
5958
fileBytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
60-
// place the index one directory too high
61-
metadataType := "META-INF/statedb/couchdb"
6259

63-
err := ValidateMetadataFile(fileName, fileBytes, metadataType)
60+
err := ValidateMetadataFile(fileName, fileBytes)
6461
assert.Error(t, err, "Should have received an UnhandledDirectoryError")
6562

6663
// Type assertion on UnhandledDirectoryError
@@ -77,9 +74,8 @@ func TestInvalidMetadataType(t *testing.T) {
7774

7875
fileName := "myIndex.json"
7976
fileBytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
80-
metadataType := "Invalid metadata type"
8177

82-
err := ValidateMetadataFile(fileName, fileBytes, metadataType)
78+
err := ValidateMetadataFile(fileName, fileBytes)
8379
assert.Error(t, err, "Should have received an UnhandledDirectoryError")
8480

8581
// Type assertion on UnhandledDirectoryError
@@ -94,14 +90,80 @@ func TestBadMetadataExtension(t *testing.T) {
9490

9591
fileName := "myIndex.go"
9692
fileBytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
97-
metadataType := "META-INF/statedb/couchdb/indexes"
9893

99-
err := ValidateMetadataFile(fileName, fileBytes, metadataType)
100-
assert.Error(t, err, "Should have received an BadExtensionError")
94+
err := ValidateMetadataFile(fileName, fileBytes)
95+
assert.Error(t, err, "Should have received an error")
96+
97+
}
98+
99+
func TestBadFilePaths(t *testing.T) {
100+
testDir := filepath.Join(packageTestDir, "BadMetadataExtension")
101+
cleanupDir(testDir)
102+
defer cleanupDir(testDir)
103+
104+
// Test bad META-INF
105+
fileName := "META-INF1/statedb/couchdb/indexes/test1.json"
106+
fileBytes := []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
107+
108+
err := ValidateMetadataFile(fileName, fileBytes)
109+
fmt.Println(err)
110+
assert.Error(t, err, "Should have received an error for bad META-INF directory")
111+
112+
// Test bad path length
113+
fileName = "META-INF/statedb/test1.json"
114+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
115+
116+
err = ValidateMetadataFile(fileName, fileBytes)
117+
fmt.Println(err)
118+
assert.Error(t, err, "Should have received an error for bad length")
119+
120+
// Test invalid database name
121+
fileName = "META-INF/statedb/goleveldb/indexes/test1.json"
122+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
123+
124+
err = ValidateMetadataFile(fileName, fileBytes)
125+
fmt.Println(err)
126+
assert.Error(t, err, "Should have received an error for invalid database")
127+
128+
// Test invalid indexes directory name
129+
fileName = "META-INF/statedb/couchdb/index/test1.json"
130+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
131+
132+
err = ValidateMetadataFile(fileName, fileBytes)
133+
fmt.Println(err)
134+
assert.Error(t, err, "Should have received an error for invalid indexes directory")
135+
136+
// Test invalid collections directory name
137+
fileName = "META-INF/statedb/couchdb/collection/testcoll/indexes/test1.json"
138+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
139+
140+
err = ValidateMetadataFile(fileName, fileBytes)
141+
fmt.Println(err)
142+
assert.Error(t, err, "Should have received an error for invalid collections directory")
143+
144+
// Test valid collections name
145+
fileName = "META-INF/statedb/couchdb/collections/testcoll/indexes/test1.json"
146+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
147+
148+
err = ValidateMetadataFile(fileName, fileBytes)
149+
fmt.Println(err)
150+
assert.NoError(t, err, "Error should not have been thrown for a valid collection name")
151+
152+
// Test invalid collections name
153+
fileName = "META-INF/statedb/couchdb/collections/#testcoll/indexes/test1.json"
154+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
155+
156+
err = ValidateMetadataFile(fileName, fileBytes)
157+
fmt.Println(err)
158+
assert.Error(t, err, "Should have received an error for an invalid collection name")
159+
160+
// Test invalid collections name
161+
fileName = "META-INF/statedb/couchdb/collections/testcoll/indexes/test1.txt"
162+
fileBytes = []byte(`{"index":{"fields":["data.docType","data.owner"]},"name":"indexOwner","type":"json"}`)
101163

102-
// Type assertion on BadExtensionError
103-
_, ok := err.(*BadExtensionError)
104-
assert.True(t, ok, "Should have received an BadExtensionError")
164+
err = ValidateMetadataFile(fileName, fileBytes)
165+
fmt.Println(err)
166+
assert.Error(t, err, "Should have received an error for an invalid file name")
105167

106168
}
107169

core/chaincode/platforms/golang/platform.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ func (goPlatform *Platform) GetDeploymentPayload(spec *pb.ChaincodeSpec) ([]byte
442442
}
443443

444444
// Split the tar location (file.Name) into a tar package directory and filename
445-
packageDir, filename := filepath.Split(file.Name)
445+
_, filename := filepath.Split(file.Name)
446446

447447
// Hidden files are not supported as metadata, therefore ignore them.
448448
// 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.
@@ -457,9 +457,8 @@ func (goPlatform *Platform) GetDeploymentPayload(spec *pb.ChaincodeSpec) ([]byte
457457
}
458458

459459
// Validate metadata file for inclusion in tar
460-
// Validation is based on the passed metadata directory, e.g. META-INF/statedb/couchdb/indexes
461-
// Clean metadata directory to remove trailing slash
462-
err = ccmetadata.ValidateMetadataFile(filename, fileBytes, filepath.Clean(packageDir))
460+
// Validation is based on the passed filename with path
461+
err = ccmetadata.ValidateMetadataFile(file.Name, fileBytes)
463462
if err != nil {
464463
return nil, err
465464
}

0 commit comments

Comments
 (0)