github.com/Hnampk/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  }