github.com/lzy4123/fabric@v2.1.1+incompatible/internal/ccmetadata/validators.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package ccmetadata 8 9 import ( 10 "encoding/json" 11 "fmt" 12 "path/filepath" 13 "reflect" 14 "regexp" 15 "strings" 16 17 "github.com/hyperledger/fabric/common/flogging" 18 ) 19 20 var logger = flogging.MustGetLogger("chaincode.platform.metadata") 21 22 // fileValidators are used as handlers to validate specific metadata directories 23 type fileValidator func(fileName string, fileBytes []byte) error 24 25 // AllowedCharsCollectionName captures the regex pattern for a valid collection name 26 const AllowedCharsCollectionName = "[A-Za-z0-9_-]+" 27 28 // Currently, the only metadata expected and allowed is for META-INF/statedb/couchdb/indexes. 29 var fileValidators = map[*regexp.Regexp]fileValidator{ 30 regexp.MustCompile("^META-INF/statedb/couchdb/indexes/.*[.]json"): couchdbIndexFileValidator, 31 regexp.MustCompile("^META-INF/statedb/couchdb/collections/" + AllowedCharsCollectionName + "/indexes/.*[.]json"): couchdbIndexFileValidator, 32 } 33 34 var collectionNameValid = regexp.MustCompile("^" + AllowedCharsCollectionName) 35 36 var fileNameValid = regexp.MustCompile("^.*[.]json") 37 38 var validDatabases = []string{"couchdb"} 39 40 // UnhandledDirectoryError is returned for metadata files in unhandled directories 41 type UnhandledDirectoryError struct { 42 err string 43 } 44 45 func (e *UnhandledDirectoryError) Error() string { 46 return e.err 47 } 48 49 // InvalidIndexContentError is returned for metadata files with invalid content 50 type InvalidIndexContentError struct { 51 err string 52 } 53 54 func (e *InvalidIndexContentError) Error() string { 55 return e.err 56 } 57 58 // ValidateMetadataFile checks that metadata files are valid 59 // according to the validation rules of the file's directory 60 func ValidateMetadataFile(filePathName string, fileBytes []byte) error { 61 // Get the validator handler for the metadata directory 62 fileValidator := selectFileValidator(filePathName) 63 64 // If there is no validator handler for metadata directory, return UnhandledDirectoryError 65 if fileValidator == nil { 66 return &UnhandledDirectoryError{buildMetadataFileErrorMessage(filePathName)} 67 } 68 69 // If the file is not valid for the given directory-based validator, return the corresponding error 70 err := fileValidator(filePathName, fileBytes) 71 if err != nil { 72 return err 73 } 74 75 // file is valid, return nil error 76 return nil 77 } 78 79 func buildMetadataFileErrorMessage(filePathName string) string { 80 81 dir, filename := filepath.Split(filePathName) 82 83 if !strings.HasPrefix(filePathName, "META-INF/statedb") { 84 return fmt.Sprintf("metadata file path must begin with META-INF/statedb, found: %s", dir) 85 } 86 directoryArray := strings.Split(filepath.Clean(dir), "/") 87 // verify the minimum directory depth 88 if len(directoryArray) < 4 { 89 return fmt.Sprintf("metadata file path must include a database and index directory: %s", dir) 90 } 91 // validate the database type 92 if !contains(validDatabases, directoryArray[2]) { 93 return fmt.Sprintf("database name [%s] is not supported, valid options: %s", directoryArray[2], validDatabases) 94 } 95 // verify "indexes" is under the database name 96 if len(directoryArray) == 4 && directoryArray[3] != "indexes" { 97 return fmt.Sprintf("metadata file path does not have an indexes directory: %s", dir) 98 } 99 // if this is for collections, check the path length 100 if len(directoryArray) != 6 { 101 return fmt.Sprintf("metadata file path for collections must include a collections and index directory: %s", dir) 102 } 103 // verify "indexes" is under the collections and collection directories 104 if directoryArray[3] != "collections" || directoryArray[5] != "indexes" { 105 return fmt.Sprintf("metadata file path for collections must have a collections and indexes directory: %s", dir) 106 } 107 // validate the collection name 108 if !collectionNameValid.MatchString(directoryArray[4]) { 109 return fmt.Sprintf("collection name is not valid: %s", directoryArray[4]) 110 } 111 112 // validate the file name 113 if !fileNameValid.MatchString(filename) { 114 return fmt.Sprintf("artifact file name is not valid: %s", filename) 115 } 116 117 return fmt.Sprintf("metadata file path or name is not supported: %s", dir) 118 119 } 120 121 func contains(validStrings []string, target string) bool { 122 for _, str := range validStrings { 123 if str == target { 124 return true 125 } 126 } 127 return false 128 } 129 130 func selectFileValidator(filePathName string) fileValidator { 131 for validateExp, fileValidator := range fileValidators { 132 isValid := validateExp.MatchString(filePathName) 133 if isValid { 134 return fileValidator 135 } 136 } 137 return nil 138 } 139 140 // couchdbIndexFileValidator implements fileValidator 141 func couchdbIndexFileValidator(fileName string, fileBytes []byte) error { 142 143 // if the content does not validate as JSON, return err to invalidate the file 144 boolIsJSON, indexDefinition := isJSON(fileBytes) 145 if !boolIsJSON { 146 return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid JSON", fileName)} 147 } 148 149 // validate the index definition 150 err := validateIndexJSON(indexDefinition) 151 if err != nil { 152 return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid index definition: %s", fileName, err)} 153 } 154 155 return nil 156 157 } 158 159 // isJSON tests a string to determine if it can be parsed as valid JSON 160 func isJSON(s []byte) (bool, map[string]interface{}) { 161 var js map[string]interface{} 162 return json.Unmarshal([]byte(s), &js) == nil, js 163 } 164 165 func validateIndexJSON(indexDefinition map[string]interface{}) error { 166 167 //flag to track if the "index" key is included 168 indexIncluded := false 169 170 //iterate through the JSON index definition 171 for jsonKey, jsonValue := range indexDefinition { 172 173 //create a case for the top level entries 174 switch jsonKey { 175 176 case "index": 177 178 if reflect.TypeOf(jsonValue).Kind() != reflect.Map { 179 return fmt.Errorf("Invalid entry, \"index\" must be a JSON") 180 } 181 182 err := processIndexMap(jsonValue.(map[string]interface{})) 183 if err != nil { 184 return err 185 } 186 187 indexIncluded = true 188 189 case "ddoc": 190 191 //Verify the design doc is a string 192 if reflect.TypeOf(jsonValue).Kind() != reflect.String { 193 return fmt.Errorf("Invalid entry, \"ddoc\" must be a string") 194 } 195 196 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 197 198 case "name": 199 200 //Verify the name is a string 201 if reflect.TypeOf(jsonValue).Kind() != reflect.String { 202 return fmt.Errorf("Invalid entry, \"name\" must be a string") 203 } 204 205 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 206 207 case "type": 208 209 if jsonValue != "json" { 210 return fmt.Errorf("Index type must be json") 211 } 212 213 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 214 215 default: 216 217 return fmt.Errorf("Invalid Entry. Entry %s", jsonKey) 218 219 } 220 221 } 222 223 if !indexIncluded { 224 return fmt.Errorf("Index definition must include a \"fields\" definition") 225 } 226 227 return nil 228 229 } 230 231 //processIndexMap processes an interface map and wraps field names or traverses 232 //the next level of the json query 233 func processIndexMap(jsonFragment map[string]interface{}) error { 234 235 //iterate the item in the map 236 for jsonKey, jsonValue := range jsonFragment { 237 238 switch jsonKey { 239 240 case "fields": 241 242 switch jsonValueType := jsonValue.(type) { 243 244 case []interface{}: 245 246 //iterate the index field objects 247 for _, itemValue := range jsonValueType { 248 249 switch reflect.TypeOf(itemValue).Kind() { 250 251 case reflect.String: 252 //String is a valid field descriptor ex: "color", "size" 253 logger.Debugf("Found index field name: \"%s\"", itemValue) 254 255 case reflect.Map: 256 //Handle the case where a sort is included ex: {"size":"asc"}, {"color":"desc"} 257 err := validateFieldMap(itemValue.(map[string]interface{})) 258 if err != nil { 259 return err 260 } 261 262 } 263 } 264 265 default: 266 return fmt.Errorf("Expecting a JSON array of fields") 267 } 268 269 case "partial_filter_selector": 270 271 //TODO - add support for partial filter selector, for now return nil 272 //Take no other action, will be considered valid for now 273 274 default: 275 276 //if anything other than "fields" or "partial_filter_selector" was found, 277 //return an error 278 return fmt.Errorf("Invalid Entry. Entry %s", jsonKey) 279 280 } 281 282 } 283 284 return nil 285 286 } 287 288 //validateFieldMap validates the list of field objects 289 func validateFieldMap(jsonFragment map[string]interface{}) error { 290 291 //iterate the fields to validate the sort criteria 292 for jsonKey, jsonValue := range jsonFragment { 293 294 switch jsonValue.(type) { 295 296 case string: 297 //Ensure the sort is either "asc" or "desc" 298 if !(strings.ToLower(jsonValue.(string)) == "asc" || strings.ToLower(jsonValue.(string)) == "desc") { 299 return fmt.Errorf("Sort must be either \"asc\" or \"desc\". \"%s\" was found.", jsonValue) 300 } 301 logger.Debugf("Found index field name: \"%s\":\"%s\"", jsonKey, jsonValue) 302 303 default: 304 return fmt.Errorf("Invalid field definition, fields must be in the form \"fieldname\":\"sort\"") 305 306 } 307 } 308 309 return nil 310 311 }