From de022f1edf9940694a81e12ef05771a318c3fac7 Mon Sep 17 00:00:00 2001 From: Tristan Cartledge <108070248+TristanSpeakEasy@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:48:28 +0100 Subject: [PATCH] feat: support validation options specifically for disabling pattern validation (#590) --- README.md | 3 ++ openapi3/loader.go | 6 ++-- openapi3/openapi3.go | 9 ++++- openapi3/schema.go | 18 +++++----- openapi3/schema_test.go | 7 ++-- openapi3/schema_validation_settings.go | 19 ++++++++-- openapi3/testdata/issue409.yml | 21 +++++++++++ openapi3/validation_issue409_test.go | 50 ++++++++++++++++++++++++++ openapi3/validation_options.go | 40 +++++++++++++++++++++ 9 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 openapi3/testdata/issue409.yml create mode 100644 openapi3/validation_issue409_test.go create mode 100644 openapi3/validation_options.go diff --git a/README.md b/README.md index a829e5983..ae87783a2 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ## Sub-v0 breaking API changes +### v0.101.0 +* `openapi3.SchemaFormatValidationDisabled` has been removed in favour of an option `openapi3.EnableSchemaFormatValidation()` passed to `openapi3.T.Validate`. The default behaviour is also now to not validate formats, as the OpenAPI spec mentions the `format` is an open value. + ### v0.84.0 * The prototype of `openapi3gen.NewSchemaRefForValue` changed: * It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`. diff --git a/openapi3/loader.go b/openapi3/loader.go index 806b819df..87c9f8684 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -166,6 +166,10 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR // ResolveRefsIn expands references if for instance spec was just unmarshalled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { + if loader.Context == nil { + loader.Context = context.Background() + } + if loader.visitedPathItemRefs == nil { loader.resetVisitedPathItemRefs() } @@ -406,7 +410,6 @@ func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef return current } return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)} - } func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { @@ -837,7 +840,6 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { - if component == nil { return errors.New("invalid callback: value MUST be an object") } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index cb9183d47..20549c2b7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -54,7 +54,14 @@ func (doc *T) AddServer(server *Server) { } // Validate returns an error if T does not comply with the OpenAPI spec. -func (doc *T) Validate(ctx context.Context) error { +// Validations Options can be provided to modify the validation behavior. +func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { + validationOpts := &ValidationOptions{} + for _, opt := range opts { + opt(validationOpts) + } + ctx = WithValidationOptions(ctx, validationOpts) + if doc.OpenAPI == "" { return errors.New("value of openapi must be a non-empty string") } diff --git a/openapi3/schema.go b/openapi3/schema.go index 17f547e66..57f63fbd8 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -36,9 +36,6 @@ var ( // SchemaErrorDetailsDisabled disables printing of details about schema errors. SchemaErrorDetailsDisabled = false - //SchemaFormatValidationDisabled disables validation of schema type formats. - SchemaFormatValidationDisabled = false - errSchema = errors.New("input does not match the schema") // ErrOneOfConflict is the SchemaError Origin when data matches more than one oneOf schema @@ -403,6 +400,7 @@ func (schema *Schema) WithMax(value float64) *Schema { schema.Max = &value return schema } + func (schema *Schema) WithExclusiveMin(value bool) *Schema { schema.ExclusiveMin = value return schema @@ -606,6 +604,8 @@ func (schema *Schema) Validate(ctx context.Context) error { } func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { + validationOpts := getValidationOptions(ctx) + for _, existing := range stack { if existing == schema { return @@ -666,7 +666,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "float", "double": default: - if !SchemaFormatValidationDisabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -676,7 +676,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) switch format { case "int32", "int64": default: - if !SchemaFormatValidationDisabled { + if validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } @@ -698,12 +698,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled { + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.SchemaFormatValidationEnabled { return unsupportedFormat(format) } } } - if schema.Pattern != "" { + if schema.Pattern != "" && !validationOpts.SchemaPatternValidationDisabled { if err = schema.compilePattern(); err != nil { return err } @@ -1063,7 +1063,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value formatMin = formatMinInt64 formatMax = formatMaxInt64 default: - if !SchemaFormatValidationDisabled { + if settings.formatValidationEnabled { return unsupportedFormat(schema.Format) } } @@ -1237,7 +1237,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value } // "pattern" - if schema.Pattern != "" && schema.compiledPattern == nil { + if schema.Pattern != "" && schema.compiledPattern == nil && !settings.patternValidationDisabled { var err error if err = schema.compilePattern(); err != nil { if !settings.multiError { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 593fa17aa..4c14dcb10 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -1028,7 +1028,10 @@ func testType(t *testing.T, example schemaTypeExample) func(*testing.T) { } for _, typ := range example.AllInvalid { schema := baseSchema.WithFormat(typ) - err := schema.Validate(context.Background()) + ctx := WithValidationOptions(context.Background(), &ValidationOptions{ + SchemaFormatValidationEnabled: true, + }) + err := schema.Validate(ctx) require.Error(t, err) } } @@ -1308,6 +1311,6 @@ func TestValidationFailsOnInvalidPattern(t *testing.T) { Type: "string", } - var err = schema.Validate(context.Background()) + err := schema.Validate(context.Background()) require.Error(t, err) } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index cb4c142a4..854ae8480 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -8,9 +8,11 @@ import ( type SchemaValidationOption func(*schemaValidationSettings) type schemaValidationSettings struct { - failfast bool - multiError bool - asreq, asrep bool // exclusive (XOR) fields + failfast bool + multiError bool + asreq, asrep bool // exclusive (XOR) fields + formatValidationEnabled bool + patternValidationDisabled bool onceSettingDefaults sync.Once defaultsSet func() @@ -28,10 +30,21 @@ func MultiErrors() SchemaValidationOption { func VisitAsRequest() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false } } + func VisitAsResponse() SchemaValidationOption { return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true } } +// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableFormatValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.formatValidationEnabled = true } +} + +// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. +func DisablePatternValidation() SchemaValidationOption { + return func(s *schemaValidationSettings) { s.patternValidationDisabled = true } +} + // DefaultsSet executes the given callback (once) IFF schema validation set default values. func DefaultsSet(f func()) SchemaValidationOption { return func(s *schemaValidationSettings) { s.defaultsSet = f } diff --git a/openapi3/testdata/issue409.yml b/openapi3/testdata/issue409.yml new file mode 100644 index 000000000..88394904e --- /dev/null +++ b/openapi3/testdata/issue409.yml @@ -0,0 +1,21 @@ +openapi: 3.0.3 +info: + description: Contains Patterns that can't be compiled by the go regexp engine + title: Issue 409 + version: 0.0.1 +paths: + /v1/apis/{apiID}: + get: + description: Get a list of all Apis and there versions for a given workspace + operationId: getApisV1 + parameters: + - description: The ID of the API + in: path + name: apiID + required: true + schema: + type: string + pattern: ^[a-zA-Z0-9]{0,4096}$ + responses: + "200": + description: OK diff --git a/openapi3/validation_issue409_test.go b/openapi3/validation_issue409_test.go new file mode 100644 index 000000000..561594fca --- /dev/null +++ b/openapi3/validation_issue409_test.go @@ -0,0 +1,50 @@ +package openapi3_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue409PatternIgnored(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} + +func TestIssue409PatternNotIgnored(t *testing.T) { + l := openapi3.NewLoader() + s, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = s.Validate(l.Context) + assert.Error(t, err) +} + +func TestIssue409HygienicUseOfCtx(t *testing.T) { + l := openapi3.NewLoader() + doc, err := l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) + err = doc.Validate(l.Context) + assert.Error(t, err) + + // and the other way + + l = openapi3.NewLoader() + doc, err = l.LoadFromFile("testdata/issue409.yml") + require.NoError(t, err) + + err = doc.Validate(l.Context) + assert.Error(t, err) + err = doc.Validate(l.Context, openapi3.DisableSchemaPatternValidation()) + assert.NoError(t, err) +} diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go new file mode 100644 index 000000000..f6038ed10 --- /dev/null +++ b/openapi3/validation_options.go @@ -0,0 +1,40 @@ +package openapi3 + +import "context" + +// ValidationOption allows the modification of how the OpenAPI document is validated. +type ValidationOption func(options *ValidationOptions) + +// ValidationOptions provide configuration for validating OpenAPI documents. +type ValidationOptions struct { + SchemaFormatValidationEnabled bool + SchemaPatternValidationDisabled bool +} + +type validationOptionsKey struct{} + +// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. +func EnableSchemaFormatValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaFormatValidationEnabled = true + } +} + +// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine. +func DisableSchemaPatternValidation() ValidationOption { + return func(options *ValidationOptions) { + options.SchemaPatternValidationDisabled = true + } +} + +// WithValidationOptions allows adding validation options to a context object that can be used when validationg any OpenAPI type. +func WithValidationOptions(ctx context.Context, options *ValidationOptions) context.Context { + return context.WithValue(ctx, validationOptionsKey{}, options) +} + +func getValidationOptions(ctx context.Context) *ValidationOptions { + if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok { + return options + } + return &ValidationOptions{} +}