296 lines
8.6 KiB
Go
296 lines
8.6 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package ccmetadata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/hyperledger/fabric/common/flogging"
|
|
)
|
|
|
|
var logger = flogging.MustGetLogger("chaincode.platform.metadata")
|
|
|
|
// fileValidators are used as handlers to validate specific metadata directories
|
|
type fileValidator func(fileName string, fileBytes []byte) error
|
|
|
|
// AllowedCharsCollectionName captures the regex pattern for a valid collection name
|
|
const AllowedCharsCollectionName = "[A-Za-z0-9_-]+"
|
|
|
|
// Currently, the only metadata expected and allowed is for META-INF/statedb/couchdb/indexes.
|
|
var fileValidators = map[*regexp.Regexp]fileValidator{
|
|
regexp.MustCompile("^META-INF/statedb/couchdb/indexes/.*[.]json"): couchdbIndexFileValidator,
|
|
regexp.MustCompile("^META-INF/statedb/couchdb/collections/" + AllowedCharsCollectionName + "/indexes/.*[.]json"): couchdbIndexFileValidator,
|
|
}
|
|
|
|
var collectionNameValid = regexp.MustCompile("^" + AllowedCharsCollectionName)
|
|
|
|
var fileNameValid = regexp.MustCompile("^.*[.]json")
|
|
|
|
var validDatabases = []string{"couchdb"}
|
|
|
|
// UnhandledDirectoryError is returned for metadata files in unhandled directories
|
|
type UnhandledDirectoryError struct {
|
|
err string
|
|
}
|
|
|
|
func (e *UnhandledDirectoryError) Error() string {
|
|
return e.err
|
|
}
|
|
|
|
// InvalidIndexContentError is returned for metadata files with invalid content
|
|
type InvalidIndexContentError struct {
|
|
err string
|
|
}
|
|
|
|
func (e *InvalidIndexContentError) Error() string {
|
|
return e.err
|
|
}
|
|
|
|
// ValidateMetadataFile checks that metadata files are valid
|
|
// according to the validation rules of the file's directory
|
|
func ValidateMetadataFile(filePathName string, fileBytes []byte) error {
|
|
// Get the validator handler for the metadata directory
|
|
fileValidator := selectFileValidator(filePathName)
|
|
|
|
// If there is no validator handler for metadata directory, return UnhandledDirectoryError
|
|
if fileValidator == nil {
|
|
return &UnhandledDirectoryError{buildMetadataFileErrorMessage(filePathName)}
|
|
}
|
|
|
|
// If the file is not valid for the given directory-based validator, return the corresponding error
|
|
err := fileValidator(filePathName, fileBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// file is valid, return nil error
|
|
return nil
|
|
}
|
|
|
|
func buildMetadataFileErrorMessage(filePathName string) string {
|
|
dir, filename := filepath.Split(filePathName)
|
|
|
|
if !strings.HasPrefix(filePathName, "META-INF/statedb") {
|
|
return fmt.Sprintf("metadata file path must begin with META-INF/statedb, found: %s", dir)
|
|
}
|
|
directoryArray := strings.Split(filepath.Clean(dir), "/")
|
|
// verify the minimum directory depth
|
|
if len(directoryArray) < 4 {
|
|
return fmt.Sprintf("metadata file path must include a database and index directory: %s", dir)
|
|
}
|
|
// validate the database type
|
|
if !contains(validDatabases, directoryArray[2]) {
|
|
return fmt.Sprintf("database name [%s] is not supported, valid options: %s", directoryArray[2], validDatabases)
|
|
}
|
|
// verify "indexes" is under the database name
|
|
if len(directoryArray) == 4 && directoryArray[3] != "indexes" {
|
|
return fmt.Sprintf("metadata file path does not have an indexes directory: %s", dir)
|
|
}
|
|
// if this is for collections, check the path length
|
|
if len(directoryArray) != 6 {
|
|
return fmt.Sprintf("metadata file path for collections must include a collections and index directory: %s", dir)
|
|
}
|
|
// verify "indexes" is under the collections and collection directories
|
|
if directoryArray[3] != "collections" || directoryArray[5] != "indexes" {
|
|
return fmt.Sprintf("metadata file path for collections must have a collections and indexes directory: %s", dir)
|
|
}
|
|
// validate the collection name
|
|
if !collectionNameValid.MatchString(directoryArray[4]) {
|
|
return fmt.Sprintf("collection name is not valid: %s", directoryArray[4])
|
|
}
|
|
|
|
// validate the file name
|
|
if !fileNameValid.MatchString(filename) {
|
|
return fmt.Sprintf("artifact file name is not valid: %s", filename)
|
|
}
|
|
|
|
return fmt.Sprintf("metadata file path or name is not supported: %s", dir)
|
|
}
|
|
|
|
func contains(validStrings []string, target string) bool {
|
|
for _, str := range validStrings {
|
|
if str == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func selectFileValidator(filePathName string) fileValidator {
|
|
for validateExp, fileValidator := range fileValidators {
|
|
isValid := validateExp.MatchString(filePathName)
|
|
if isValid {
|
|
return fileValidator
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// couchdbIndexFileValidator implements fileValidator
|
|
func couchdbIndexFileValidator(fileName string, fileBytes []byte) error {
|
|
// if the content does not validate as JSON, return err to invalidate the file
|
|
boolIsJSON, indexDefinition := isJSON(fileBytes)
|
|
if !boolIsJSON {
|
|
return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid JSON", fileName)}
|
|
}
|
|
|
|
// validate the index definition
|
|
err := validateIndexJSON(indexDefinition)
|
|
if err != nil {
|
|
return &InvalidIndexContentError{fmt.Sprintf("Index metadata file [%s] is not a valid index definition: %s", fileName, err)}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isJSON tests a string to determine if it can be parsed as valid JSON
|
|
func isJSON(s []byte) (bool, map[string]interface{}) {
|
|
var js map[string]interface{}
|
|
return json.Unmarshal([]byte(s), &js) == nil, js
|
|
}
|
|
|
|
func validateIndexJSON(indexDefinition map[string]interface{}) error {
|
|
// flag to track if the "index" key is included
|
|
indexIncluded := false
|
|
|
|
// iterate through the JSON index definition
|
|
for jsonKey, jsonValue := range indexDefinition {
|
|
// create a case for the top level entries
|
|
switch jsonKey {
|
|
|
|
case "index":
|
|
|
|
if reflect.TypeOf(jsonValue).Kind() != reflect.Map {
|
|
return fmt.Errorf("Invalid entry, \"index\" must be a JSON")
|
|
}
|
|
|
|
err := processIndexMap(jsonValue.(map[string]interface{}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
indexIncluded = true
|
|
|
|
case "ddoc":
|
|
|
|
// Verify the design doc is a string
|
|
if reflect.TypeOf(jsonValue).Kind() != reflect.String {
|
|
return fmt.Errorf("Invalid entry, \"ddoc\" must be a string")
|
|
}
|
|
|
|
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
|
|
|
|
case "name":
|
|
|
|
// Verify the name is a string
|
|
if reflect.TypeOf(jsonValue).Kind() != reflect.String {
|
|
return fmt.Errorf("Invalid entry, \"name\" must be a string")
|
|
}
|
|
|
|
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
|
|
|
|
case "type":
|
|
|
|
if jsonValue != "json" {
|
|
return fmt.Errorf("Index type must be json")
|
|
}
|
|
|
|
logger.Debugf("Found index object: \"%s\":\"%s\"", jsonKey, jsonValue)
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("Invalid Entry. Entry %s", jsonKey)
|
|
|
|
}
|
|
}
|
|
|
|
if !indexIncluded {
|
|
return fmt.Errorf("Index definition must include a \"fields\" definition")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// processIndexMap processes an interface map and wraps field names or traverses
|
|
// the next level of the json query
|
|
func processIndexMap(jsonFragment map[string]interface{}) error {
|
|
// iterate the item in the map
|
|
for jsonKey, jsonValue := range jsonFragment {
|
|
switch jsonKey {
|
|
|
|
case "fields":
|
|
|
|
switch jsonValueType := jsonValue.(type) {
|
|
|
|
case []interface{}:
|
|
|
|
// iterate the index field objects
|
|
for _, itemValue := range jsonValueType {
|
|
switch reflect.TypeOf(itemValue).Kind() {
|
|
|
|
case reflect.String:
|
|
// String is a valid field descriptor ex: "color", "size"
|
|
logger.Debugf("Found index field name: \"%s\"", itemValue)
|
|
|
|
case reflect.Map:
|
|
// Handle the case where a sort is included ex: {"size":"asc"}, {"color":"desc"}
|
|
err := validateFieldMap(itemValue.(map[string]interface{}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("Expecting a JSON array of fields")
|
|
}
|
|
|
|
case "partial_filter_selector":
|
|
|
|
// TODO - add support for partial filter selector, for now return nil
|
|
// Take no other action, will be considered valid for now
|
|
|
|
default:
|
|
|
|
// if anything other than "fields" or "partial_filter_selector" was found,
|
|
// return an error
|
|
return fmt.Errorf("Invalid Entry. Entry %s", jsonKey)
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateFieldMap validates the list of field objects
|
|
func validateFieldMap(jsonFragment map[string]interface{}) error {
|
|
// iterate the fields to validate the sort criteria
|
|
for jsonKey, jsonValue := range jsonFragment {
|
|
switch jsonValue := jsonValue.(type) {
|
|
|
|
case string:
|
|
// Ensure the sort is either "asc" or "desc"
|
|
jv := strings.ToLower(jsonValue)
|
|
if jv != "asc" && jv != "desc" {
|
|
return fmt.Errorf("Sort must be either \"asc\" or \"desc\". \"%s\" was found.", jsonValue)
|
|
}
|
|
logger.Debugf("Found index field name: \"%s\":\"%s\"", jsonKey, jsonValue)
|
|
|
|
default:
|
|
return fmt.Errorf("Invalid field definition, fields must be in the form \"fieldname\":\"sort\"")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|