move custom metadata validation logic to its own package (#16464)
* move custom metadata validation logic to its own package * add comments * add custom metadata Validate unit tests
This commit is contained in:
parent
488858e919
commit
013e1d12b1
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,23 +6,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/protobuf/ptypes"
|
"github.com/golang/protobuf/ptypes"
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/identity"
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/helper/storagepacker"
|
"github.com/hashicorp/vault/helper/storagepacker"
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/custommetadata"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"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.
|
// aliasPaths returns the API endpoints to operate on aliases.
|
||||||
// Following are the paths supported:
|
// Following are the paths supported:
|
||||||
// entity-alias - To register/modify an alias
|
// entity-alias - To register/modify an alias
|
||||||
|
@ -152,7 +144,7 @@ func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc {
|
||||||
|
|
||||||
// validate customMetadata if provided
|
// validate customMetadata if provided
|
||||||
if len(customMetadata) != 0 {
|
if len(customMetadata) != 0 {
|
||||||
if err := validateCustomMetadata(customMetadata); err != nil {
|
if err := custommetadata.Validate(customMetadata); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -468,55 +460,6 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name
|
||||||
}, nil
|
}, 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
|
// pathAliasIDRead returns the properties of an alias for a given
|
||||||
// alias ID
|
// alias ID
|
||||||
func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc {
|
func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc {
|
||||||
|
|
Loading…
Reference in New Issue