package custommetadata import ( "fmt" "github.com/mitchellh/mapstructure" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/strutil" ) // The following constants are used by Validate and are meant to be imposed // broadly for consistency. const ( maxKeys = 64 maxKeyLength = 128 maxValueLength = 512 validationErrorPrefix = "custom_metadata validation failed" ) // Parse is used to effectively convert the TypeMap // (map[string]interface{}) into a TypeKVPairs (map[string]string) // which is how custom_metadata is stored. Defining custom_metadata // as a TypeKVPairs will convert nulls into empty strings. A null, // however, is essential for a PATCH operation in that it signals // the handler to remove the field. The filterNils flag should // only be used during a patch operation. func Parse(raw map[string]interface{}, filterNils bool) (map[string]string, error) { customMetadata := map[string]string{} for k, v := range raw { if filterNils && v == nil { continue } var s string if err := mapstructure.WeakDecode(v, &s); err != nil { return nil, err } customMetadata[k] = s } return customMetadata, nil } // Validate will perform input validation for custom metadata. // CustomMetadata should be arbitrary user-provided key-value pairs meant to // provide supplemental information about a resource. If the key count // exceeds maxKeys, the validation will be short-circuited to prevent // unnecessary (and potentially costly) validation to be run. If the key count // falls at or below maxKeys, multiple checks will be made per key and value. // These checks include: // - 0 < length of key <= maxKeyLength // - 0 < length of value <= maxValueLength // - keys and values cannot include unprintable characters func Validate(cm map[string]string) error { var errs *multierror.Error if keyCount := len(cm); keyCount > maxKeys { errs = multierror.Append(errs, fmt.Errorf("%s: payload must contain at most %d keys, provided %d", validationErrorPrefix, maxKeys, keyCount)) return errs.ErrorOrNil() } // Perform validation on each key and value and return ALL errors for key, value := range cm { if keyLen := len(key); 0 == keyLen || keyLen > maxKeyLength { errs = multierror.Append(errs, fmt.Errorf("%s: length of key %q is %d but must be 0 < len(key) <= %d", validationErrorPrefix, key, keyLen, maxKeyLength)) } if valueLen := len(value); 0 == valueLen || valueLen > maxValueLength { errs = multierror.Append(errs, fmt.Errorf("%s: length of value for key %q is %d but must be 0 < len(value) <= %d", validationErrorPrefix, key, valueLen, maxValueLength)) } if !strutil.Printable(key) { // Include unquoted format (%s) to also include the string without the unprintable // characters visible to allow for easier debug and key identification errs = multierror.Append(errs, fmt.Errorf("%s: key %q (%s) contains unprintable characters", validationErrorPrefix, key, key)) } if !strutil.Printable(value) { errs = multierror.Append(errs, fmt.Errorf("%s: value for key %q contains unprintable characters", validationErrorPrefix, key)) } } return errs.ErrorOrNil() }