diff --git a/sdk/helper/custommetadata/custom_metadata.go b/sdk/helper/custommetadata/custom_metadata.go new file mode 100644 index 000000000..c798fedf7 --- /dev/null +++ b/sdk/helper/custommetadata/custom_metadata.go @@ -0,0 +1,78 @@ +package custommetadata + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-secure-stdlib/strutil" +) + +// CustomMetadata should be arbitrary user-provided key-value pairs meant to +// provide supplemental information about a resource. +type CustomMetadata map[string]string + +// 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" +) + +// Validate will perform input validation for custom metadata. 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 CustomMetadata) 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() +} diff --git a/sdk/helper/custommetadata/custom_metadata_test.go b/sdk/helper/custommetadata/custom_metadata_test.go new file mode 100644 index 000000000..480fab52f --- /dev/null +++ b/sdk/helper/custommetadata/custom_metadata_test.go @@ -0,0 +1,85 @@ +package custommetadata + +import ( + "strconv" + "strings" + "testing" +) + +func TestValidate(t *testing.T) { + cases := []struct { + name string + input CustomMetadata + shouldPass bool + }{ + { + "valid", + CustomMetadata{ + "foo": "abc", + "bar": "def", + "baz": "ghi", + }, + true, + }, + { + "too_many_keys", + func() CustomMetadata { + cm := make(CustomMetadata) + + for i := 0; i < maxKeyLength+1; i++ { + s := strconv.Itoa(i) + cm[s] = s + } + + return cm + }(), + false, + }, + { + "key_too_long", + CustomMetadata{ + strings.Repeat("a", maxKeyLength+1): "abc", + }, + false, + }, + { + "value_too_long", + CustomMetadata{ + "foo": strings.Repeat("a", maxValueLength+1), + }, + false, + }, + { + "unprintable_key", + CustomMetadata{ + "unprint\u200bable": "abc", + }, + false, + }, + { + "unprintable_value", + CustomMetadata{ + "foo": "unprint\u200bable", + }, + false, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := Validate(tc.input) + + if tc.shouldPass && err != nil { + t.Fatalf("expected validation to pass, input: %#v, err: %v", tc.input, err) + } + + if !tc.shouldPass && err == nil { + t.Fatalf("expected validation to fail, input: %#v, err: %v", tc.input, err) + } + }) + } +} diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index b717f83ea..1fe3c3126 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -6,23 +6,15 @@ import ( "strings" "github.com/golang/protobuf/ptypes" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/storagepacker" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/custommetadata" "github.com/hashicorp/vault/sdk/logical" ) -const ( - maxCustomMetadataKeys = 64 - maxCustomMetadataKeyLength = 128 - maxCustomMetadataValueLength = 512 - customMetadataValidationErrorPrefix = "custom_metadata validation failed" -) - // aliasPaths returns the API endpoints to operate on aliases. // Following are the paths supported: // entity-alias - To register/modify an alias @@ -152,7 +144,7 @@ func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc { // validate customMetadata if provided if len(customMetadata) != 0 { - if err := validateCustomMetadata(customMetadata); err != nil { + if err := custommetadata.Validate(customMetadata); err != nil { return nil, err } } @@ -468,55 +460,6 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name }, nil } -func validateCustomMetadata(customMetadata map[string]string) error { - var errs *multierror.Error - - if keyCount := len(customMetadata); keyCount > maxCustomMetadataKeys { - errs = multierror.Append(errs, fmt.Errorf("%s: payload must contain at most %d keys, provided %d", - customMetadataValidationErrorPrefix, - maxCustomMetadataKeys, - keyCount)) - - return errs.ErrorOrNil() - } - - // Perform validation on each key and value and return ALL errors - for key, value := range customMetadata { - if keyLen := len(key); 0 == keyLen || keyLen > maxCustomMetadataKeyLength { - errs = multierror.Append(errs, fmt.Errorf("%s: length of key %q is %d but must be 0 < len(key) <= %d", - customMetadataValidationErrorPrefix, - key, - keyLen, - maxCustomMetadataKeyLength)) - } - - if valueLen := len(value); 0 == valueLen || valueLen > maxCustomMetadataValueLength { - errs = multierror.Append(errs, fmt.Errorf("%s: length of value for key %q is %d but must be 0 < len(value) <= %d", - customMetadataValidationErrorPrefix, - key, - valueLen, - maxCustomMetadataValueLength)) - } - - 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", - customMetadataValidationErrorPrefix, - key, - key)) - } - - if !strutil.Printable(value) { - errs = multierror.Append(errs, fmt.Errorf("%s: value for key %q contains unprintable characters", - customMetadataValidationErrorPrefix, - key)) - } - } - - return errs.ErrorOrNil() -} - // pathAliasIDRead returns the properties of an alias for a given // alias ID func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc {