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  }