github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/internal/ccmetadata/validators.go (about) 1 /* 2 Copyright hechain. 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/hechain20/hechain/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 dir, filename := filepath.Split(filePathName) 81 82 if !strings.HasPrefix(filePathName, "META-INF/statedb") { 83 return fmt.Sprintf("metadata file path must begin with META-INF/statedb, found: %s", dir) 84 } 85 directoryArray := strings.Split(filepath.Clean(dir), "/") 86 // verify the minimum directory depth 87 if len(directoryArray) < 4 { 88 return fmt.Sprintf("metadata file path must include a database and index directory: %s", dir) 89 } 90 // validate the database type 91 if !contains(validDatabases, directoryArray[2]) { 92 return fmt.Sprintf("database name [%s] is not supported, valid options: %s", directoryArray[2], validDatabases) 93 } 94 // verify "indexes" is under the database name 95 if len(directoryArray) == 4 && directoryArray[3] != "indexes" { 96 return fmt.Sprintf("metadata file path does not have an indexes directory: %s", dir) 97 } 98 // if this is for collections, check the path length 99 if len(directoryArray) != 6 { 100 return fmt.Sprintf("metadata file path for collections must include a collections and index directory: %s", dir) 101 } 102 // verify "indexes" is under the collections and collection directories 103 if directoryArray[3] != "collections" || directoryArray[5] != "indexes" { 104 return fmt.Sprintf("metadata file path for collections must have a collections and indexes directory: %s", dir) 105 } 106 // validate the collection name 107 if !collectionNameValid.MatchString(directoryArray[4]) { 108 return fmt.Sprintf("collection name is not valid: %s", directoryArray[4]) 109 } 110 111 // validate the file name 112 if !fileNameValid.MatchString(filename) { 113 return fmt.Sprintf("artifact file name is not valid: %s", filename) 114 } 115 116 return fmt.Sprintf("metadata file path or name is not supported: %s", dir) 117 } 118 119 func contains(validStrings []string, target string) bool { 120 for _, str := range validStrings { 121 if str == target { 122 return true 123 } 124 } 125 return false 126 } 127 128 func selectFileValidator(filePathName string) fileValidator { 129 for validateExp, fileValidator := range fileValidators { 130 isValid := validateExp.MatchString(filePathName) 131 if isValid { 132 return fileValidator 133 } 134 } 135 return nil 136 } 137 138 // couchdbIndexFileValidator implements fileValidator 139 func couchdbIndexFileValidator(fileName string, fileBytes []byte) error { 140 // if the content does not validate as JSON, return err to invalidate the file 141 boolIsJSON, indexDefinition := isJSON(fileBytes) 142 if !boolIsJSON { 143 return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid JSON", fileName)} 144 } 145 146 // validate the index definition 147 err := validateIndexJSON(indexDefinition) 148 if err != nil { 149 return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid index definition: %s", fileName, err)} 150 } 151 152 return nil 153 } 154 155 // isJSON tests a string to determine if it can be parsed as valid JSON 156 func isJSON(s []byte) (bool, map[string]interface{}) { 157 var js map[string]interface{} 158 return json.Unmarshal([]byte(s), &js) == nil, js 159 } 160 161 func validateIndexJSON(indexDefinition map[string]interface{}) error { 162 // flag to track if the "index" key is included 163 indexIncluded := false 164 165 // iterate through the JSON index definition 166 for jsonKey, jsonValue := range indexDefinition { 167 // create a case for the top level entries 168 switch jsonKey { 169 170 case "index": 171 172 if reflect.TypeOf(jsonValue).Kind() != reflect.Map { 173 return fmt.Errorf("Invalid entry, \"index\" must be a JSON") 174 } 175 176 err := processIndexMap(jsonValue.(map[string]interface{})) 177 if err != nil { 178 return err 179 } 180 181 indexIncluded = true 182 183 case "ddoc": 184 185 // Verify the design doc is a string 186 if reflect.TypeOf(jsonValue).Kind() != reflect.String { 187 return fmt.Errorf("Invalid entry, \"ddoc\" must be a string") 188 } 189 190 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 191 192 case "name": 193 194 // Verify the name is a string 195 if reflect.TypeOf(jsonValue).Kind() != reflect.String { 196 return fmt.Errorf("Invalid entry, \"name\" must be a string") 197 } 198 199 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 200 201 case "type": 202 203 if jsonValue != "json" { 204 return fmt.Errorf("Index type must be json") 205 } 206 207 logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue) 208 209 default: 210 211 return fmt.Errorf("Invalid Entry. Entry %s", jsonKey) 212 213 } 214 } 215 216 if !indexIncluded { 217 return fmt.Errorf("Index definition must include a \"fields\" definition") 218 } 219 220 return nil 221 } 222 223 // processIndexMap processes an interface map and wraps field names or traverses 224 // the next level of the json query 225 func processIndexMap(jsonFragment map[string]interface{}) error { 226 // iterate the item in the map 227 for jsonKey, jsonValue := range jsonFragment { 228 switch jsonKey { 229 230 case "fields": 231 232 switch jsonValueType := jsonValue.(type) { 233 234 case []interface{}: 235 236 // iterate the index field objects 237 for _, itemValue := range jsonValueType { 238 switch reflect.TypeOf(itemValue).Kind() { 239 240 case reflect.String: 241 // String is a valid field descriptor ex: "color", "size" 242 logger.Debugf("Found index field name: \"%s\"", itemValue) 243 244 case reflect.Map: 245 // Handle the case where a sort is included ex: {"size":"asc"}, {"color":"desc"} 246 err := validateFieldMap(itemValue.(map[string]interface{})) 247 if err != nil { 248 return err 249 } 250 251 } 252 } 253 254 default: 255 return fmt.Errorf("Expecting a JSON array of fields") 256 } 257 258 case "partial_filter_selector": 259 260 // TODO - add support for partial filter selector, for now return nil 261 // Take no other action, will be considered valid for now 262 263 default: 264 265 // if anything other than "fields" or "partial_filter_selector" was found, 266 // return an error 267 return fmt.Errorf("Invalid Entry. Entry %s", jsonKey) 268 269 } 270 } 271 272 return nil 273 } 274 275 // validateFieldMap validates the list of field objects 276 func validateFieldMap(jsonFragment map[string]interface{}) error { 277 // iterate the fields to validate the sort criteria 278 for jsonKey, jsonValue := range jsonFragment { 279 switch jsonValue := jsonValue.(type) { 280 281 case string: 282 // Ensure the sort is either "asc" or "desc" 283 jv := strings.ToLower(jsonValue) 284 if jv != "asc" && jv != "desc" { 285 return fmt.Errorf("Sort must be either \"asc\" or \"desc\". \"%s\" was found.", jsonValue) 286 } 287 logger.Debugf("Found index field name: \"%s\":\"%s\"", jsonKey, jsonValue) 288 289 default: 290 return fmt.Errorf("Invalid field definition, fields must be in the form \"fieldname\":\"sort\"") 291 } 292 } 293 294 return nil 295 }