Skip to content

Commit e71c61b

Browse files
author
Chris Elder
committed
[FAB-7863] Add CouchDB index definition validation
CouchDB index definitions should be validated. Invalid index definitions can cause peer panic. This change adds validation for the index definition JSON with error reporting for bad formatting or invalid entries. Testing for this CR indicated an improvement was required in testing indexes. Change-Id: Iea87c1c263133f078d47d831cba730dff1b47e66 Signed-off-by: Chris Elder <chris.elder@us.ibm.com>
1 parent 7da97f4 commit e71c61b

File tree

3 files changed

+284
-6
lines changed

3 files changed

+284
-6
lines changed

core/common/ccprovider/metadata/validators.go

Lines changed: 156 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"errors"
1212
"fmt"
1313
"io/ioutil"
14+
"reflect"
15+
"strings"
1416

1517
"github.com/hyperledger/fabric/common/flogging"
1618
)
@@ -72,18 +74,166 @@ func couchdbIndexFileValidator(srcPath string) error {
7274
}
7375

7476
// if the content does not validate as JSON, return err to invalidate the file
75-
if !isJSON(string(fileBytes)) {
77+
boolIsJSON, indexDefinition := isJSON(fileBytes)
78+
if !boolIsJSON {
7679
return errors.New("File is not valid JSON")
7780
}
7881

79-
// TODO Additional validation to ensure the JSON represents a valid couchdb index definition
82+
// validate the index definition
83+
return validateIndexJSON(indexDefinition)
8084

81-
// file is a valid couchdb index definition, return nil error
82-
return nil
8385
}
8486

8587
// isJSON tests a string to determine if it can be parsed as valid JSON
86-
func isJSON(s string) bool {
88+
func isJSON(s []byte) (bool, map[string]interface{}) {
8789
var js map[string]interface{}
88-
return json.Unmarshal([]byte(s), &js) == nil
90+
return json.Unmarshal([]byte(s), &js) == nil, js
91+
}
92+
93+
func validateIndexJSON(indexDefinition map[string]interface{}) error {
94+
95+
//flag to track if the "index" key is included
96+
indexIncluded := false
97+
98+
//iterate through the JSON index definition
99+
for jsonKey, jsonValue := range indexDefinition {
100+
101+
//create a case for the top level entries
102+
switch jsonKey {
103+
104+
case "index":
105+
106+
if reflect.TypeOf(jsonValue).Kind() != reflect.Map {
107+
return fmt.Errorf("Invalid entry, \"index\" must be a JSON")
108+
}
109+
110+
err := processIndexMap(jsonValue.(map[string]interface{}))
111+
if err != nil {
112+
return err
113+
}
114+
115+
indexIncluded = true
116+
117+
case "ddoc":
118+
119+
//Verify the design doc is a string
120+
if reflect.TypeOf(jsonValue).Kind() != reflect.String {
121+
return fmt.Errorf("Invalid entry, \"ddoc\" must be a string")
122+
}
123+
124+
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
125+
126+
case "name":
127+
128+
//Verify the name is a string
129+
if reflect.TypeOf(jsonValue).Kind() != reflect.String {
130+
return fmt.Errorf("Invalid entry, \"name\" must be a string")
131+
}
132+
133+
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
134+
135+
case "type":
136+
137+
if jsonValue != "json" {
138+
return fmt.Errorf("Index type must be json")
139+
}
140+
141+
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
142+
143+
default:
144+
145+
return fmt.Errorf("Invalid Entry. Entry %s", jsonKey)
146+
147+
}
148+
149+
}
150+
151+
if !indexIncluded {
152+
return fmt.Errorf("Index definition must include a \"fields\" definition")
153+
}
154+
155+
return nil
156+
157+
}
158+
159+
//processIndexMap processes an interface map and wraps field names or traverses
160+
//the next level of the json query
161+
func processIndexMap(jsonFragment map[string]interface{}) error {
162+
163+
//iterate the item in the map
164+
for jsonKey, jsonValue := range jsonFragment {
165+
166+
switch jsonKey {
167+
168+
case "fields":
169+
170+
switch jsonValueType := jsonValue.(type) {
171+
172+
case []interface{}:
173+
174+
//iterate the index field objects
175+
for _, itemValue := range jsonValueType {
176+
177+
switch reflect.TypeOf(itemValue).Kind() {
178+
179+
case reflect.String:
180+
//String is a valid field descriptor ex: "color", "size"
181+
logger.Debugf("Found index field name: \"%s\"", itemValue)
182+
183+
case reflect.Map:
184+
//Handle the case where a sort is included ex: {"size":"asc"}, {"color":"desc"}
185+
err := validateFieldMap(itemValue.(map[string]interface{}))
186+
if err != nil {
187+
return err
188+
}
189+
190+
}
191+
}
192+
193+
default:
194+
return fmt.Errorf("Expecting a JSON array of fields")
195+
}
196+
197+
case "partial_filter_selector":
198+
199+
//TODO - add support for partial filter selector, for now return nil
200+
//Take no other action, will be considered valid for now
201+
202+
default:
203+
204+
//if anything other than "fields" or "partial_filter_selector" was found,
205+
//return an error
206+
return fmt.Errorf("Invalid Entry. Entry %s", jsonKey)
207+
208+
}
209+
210+
}
211+
212+
return nil
213+
214+
}
215+
216+
//validateFieldMap validates the list of field objects
217+
func validateFieldMap(jsonFragment map[string]interface{}) error {
218+
219+
//iterate the fields to validate the sort criteria
220+
for jsonKey, jsonValue := range jsonFragment {
221+
222+
switch jsonValue.(type) {
223+
224+
case string:
225+
//Ensure the sort is either "asc" or "desc"
226+
if !(strings.ToLower(jsonValue.(string)) == "asc" || strings.ToLower(jsonValue.(string)) == "desc") {
227+
return fmt.Errorf("Sort must be either \"asc\" or \"desc\". \"%s\" was found.", jsonValue)
228+
}
229+
logger.Debugf("Found index field name: \"%s\":\"%s\"", jsonKey, jsonValue)
230+
231+
default:
232+
return fmt.Errorf("Invalid field definition, fields must be in the form \"fieldname\":\"sort\"")
233+
234+
}
235+
}
236+
237+
return nil
238+
89239
}

core/common/ccprovider/metadata/validators_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,131 @@ func TestCantReadFile(t *testing.T) {
110110
assert.Error(t, err, "Should have received error reading file")
111111
}
112112

113+
func TestIndexValidation(t *testing.T) {
114+
115+
// Test valid index with field sorts
116+
indexDef := []byte(`{"index":{"fields":[{"size":"desc"}, {"color":"desc"}]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
117+
_, indexDefinition := isJSON(indexDef)
118+
err := validateIndexJSON(indexDefinition)
119+
assert.NoError(t, err)
120+
121+
// Test valid index without field sorts
122+
indexDef = []byte(`{"index":{"fields":["size","color"]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
123+
_, indexDefinition = isJSON(indexDef)
124+
err = validateIndexJSON(indexDefinition)
125+
assert.NoError(t, err)
126+
127+
// Test valid index without design doc, name and type
128+
indexDef = []byte(`{"index":{"fields":["size","color"]}}`)
129+
_, indexDefinition = isJSON(indexDef)
130+
err = validateIndexJSON(indexDefinition)
131+
assert.NoError(t, err)
132+
133+
// Test valid index with partial filter selector (only tests that it will not return error if included)
134+
indexDef = []byte(`{
135+
"index": {
136+
"partial_filter_selector": {
137+
"status": {
138+
"$ne": "archived"
139+
}
140+
},
141+
"fields": ["type"]
142+
},
143+
"ddoc" : "type-not-archived",
144+
"type" : "json"
145+
}`)
146+
_, indexDefinition = isJSON(indexDef)
147+
err = validateIndexJSON(indexDefinition)
148+
assert.NoError(t, err)
149+
150+
}
151+
152+
func TestIndexValidationInvalidParameters(t *testing.T) {
153+
154+
// Test numeric values passed in for parameters
155+
indexDef := []byte(`{"index":{"fields":[{"size":"desc"}, {"color":"desc"}]},"ddoc":1, "name":"indexSizeSortName","type":"json"}`)
156+
_, indexDefinition := isJSON(indexDef)
157+
err := validateIndexJSON(indexDefinition)
158+
assert.Error(t, err, "Error should have been thrown for numeric design doc")
159+
160+
// Test invalid design doc parameter
161+
indexDef = []byte(`{"index":{"fields":["size","color"]},"ddoc1":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
162+
_, indexDefinition = isJSON(indexDef)
163+
err = validateIndexJSON(indexDefinition)
164+
assert.Error(t, err, "Error should have been thrown for invalid design doc parameter")
165+
166+
// Test invalid name parameter
167+
indexDef = []byte(`{"index":{"fields":["size","color"]},"ddoc":"indexSizeSortName", "name1":"indexSizeSortName","type":"json"}`)
168+
_, indexDefinition = isJSON(indexDef)
169+
err = validateIndexJSON(indexDefinition)
170+
assert.Error(t, err, "Error should have been thrown for invalid name parameter")
171+
172+
// Test invalid type parameter, numeric
173+
indexDef = []byte(`{"index":{"fields":["size","color"]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":1}`)
174+
_, indexDefinition = isJSON(indexDef)
175+
err = validateIndexJSON(indexDefinition)
176+
assert.Error(t, err, "Error should have been thrown for numeric type parameter")
177+
178+
// Test invalid type parameter
179+
indexDef = []byte(`{"index":{"fields":["size","color"]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"text"}`)
180+
_, indexDefinition = isJSON(indexDef)
181+
err = validateIndexJSON(indexDefinition)
182+
assert.Error(t, err, "Error should have been thrown for invalid type parameter")
183+
184+
// Test invalid index parameter
185+
indexDef = []byte(`{"index1":{"fields":["size","color"]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
186+
_, indexDefinition = isJSON(indexDef)
187+
err = validateIndexJSON(indexDefinition)
188+
assert.Error(t, err, "Error should have been thrown for invalid index parameter")
189+
190+
// Test missing index parameter
191+
indexDef = []byte(`{"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
192+
_, indexDefinition = isJSON(indexDef)
193+
err = validateIndexJSON(indexDefinition)
194+
assert.Error(t, err, "Error should have been thrown for missing index parameter")
195+
196+
}
197+
198+
func TestIndexValidationInvalidFields(t *testing.T) {
199+
200+
// Test invalid fields parameter
201+
indexDef := []byte(`{"index":{"fields1":[{"size":"desc"}, {"color":"desc"}]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
202+
_, indexDefinition := isJSON(indexDef)
203+
err := validateIndexJSON(indexDefinition)
204+
assert.Error(t, err, "Error should have been thrown for invalid fields parameter")
205+
206+
// Test invalid field name (numeric)
207+
indexDef = []byte(`{"index":{"fields":["size", 1]},"ddoc1":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
208+
_, indexDefinition = isJSON(indexDef)
209+
err = validateIndexJSON(indexDefinition)
210+
assert.Error(t, err, "Error should have been thrown for field name defined as numeric")
211+
212+
// Test invalid field sort
213+
indexDef = []byte(`{"index":{"fields":[{"size":"desc1"}, {"color":"desc"}]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
214+
_, indexDefinition = isJSON(indexDef)
215+
err = validateIndexJSON(indexDefinition)
216+
assert.Error(t, err, "Error should have been thrown for invalid field sort")
217+
218+
// Test numeric in sort
219+
indexDef = []byte(`{"index":{"fields":[{"size":1}, {"color":"desc"}]},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
220+
_, indexDefinition = isJSON(indexDef)
221+
err = validateIndexJSON(indexDefinition)
222+
assert.Error(t, err, "Error should have been thrown for a numeric in field sort")
223+
224+
// Test invalid json for fields
225+
indexDef = []byte(`{"index":{"fields":"size"},"ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
226+
_, indexDefinition = isJSON(indexDef)
227+
err = validateIndexJSON(indexDefinition)
228+
assert.Error(t, err, "Error should have been thrown for invalid field json")
229+
230+
// Test missing JSON for fields
231+
indexDef = []byte(`{"index":"fields","ddoc":"indexSizeSortName", "name":"indexSizeSortName","type":"json"}`)
232+
_, indexDefinition = isJSON(indexDef)
233+
err = validateIndexJSON(indexDefinition)
234+
assert.Error(t, err, "Error should have been thrown for missing JSON for fields")
235+
236+
}
237+
113238
func cleanupDir(dir string) error {
114239
// clean up any previous files
115240
err := os.RemoveAll(dir)

core/ledger/util/couchdb/couchdb_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,9 @@ func TestIndexOperations(t *testing.T) {
10221022
err = db.CreateIndex(indexDefSize)
10231023
testutil.AssertNoError(t, err, fmt.Sprintf("Error thrown while creating an index"))
10241024

1025+
//Delay for 100ms since CouchDB index list is updated async after index create/drop
1026+
time.Sleep(100 * time.Millisecond)
1027+
10251028
//Execute a query with an index, this should succeed
10261029
_, err = db.QueryDocuments(queryString)
10271030
testutil.AssertNoError(t, err, fmt.Sprintf("Error thrown while querying with an index"))

0 commit comments

Comments
 (0)