// Copyright 2015 go-swagger maintainers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package validate import ( "bytes" "encoding/gob" "encoding/json" "fmt" "sort" "strings" "github.com/go-openapi/analysis" "github.com/go-openapi/errors" "github.com/go-openapi/jsonpointer" "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" ) // Spec validates an OpenAPI 2.0 specification document. // // Returns an error flattening in a single standard error, all validation messages. // // - TODO: $ref should not have siblings // - TODO: make sure documentation reflects all checks and warnings // - TODO: check on discriminators // - TODO: explicit message on unsupported keywords (better than "forbidden property"...) // - TODO: full list of unresolved refs // - TODO: validate numeric constraints (issue#581): this should be handled like defaults and examples // - TODO: option to determine if we validate for go-swagger or in a more general context // - TODO: check on required properties to support anyOf, allOf, oneOf // // NOTE: SecurityScopes are maps: no need to check uniqueness func Spec(doc *loads.Document, formats strfmt.Registry) error { errs, _ /*warns*/ := NewSpecValidator(doc.Schema(), formats).Validate(doc) if errs.HasErrors() { return errors.CompositeValidationError(errs.Errors...) } return nil } // SpecValidator validates a swagger 2.0 spec type SpecValidator struct { schema *spec.Schema // swagger 2.0 schema spec *loads.Document analyzer *analysis.Spec expanded *loads.Document KnownFormats strfmt.Registry Options Opts // validation options schemaOptions *SchemaValidatorOptions } // NewSpecValidator creates a new swagger spec validator instance func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidator { // schema options that apply to all called validators schemaOptions := new(SchemaValidatorOptions) for _, o := range []Option{ SwaggerSchema(true), WithRecycleValidators(true), // withRecycleResults(true), } { o(schemaOptions) } return &SpecValidator{ schema: schema, KnownFormats: formats, Options: defaultOpts, schemaOptions: schemaOptions, } } // Validate validates the swagger spec func (s *SpecValidator) Validate(data interface{}) (*Result, *Result) { s.schemaOptions.skipSchemataResult = s.Options.SkipSchemataResult var sd *loads.Document errs, warnings := new(Result), new(Result) if v, ok := data.(*loads.Document); ok { sd = v } if sd == nil { errs.AddErrors(invalidDocumentMsg()) return errs, warnings // no point in continuing } s.spec = sd s.analyzer = analysis.New(sd.Spec()) // Raw spec unmarshalling errors var obj interface{} if err := json.Unmarshal(sd.Raw(), &obj); err != nil { // NOTE: under normal conditions, the *load.Document has been already unmarshalled // So this one is just a paranoid check on the behavior of the spec package panic(InvalidDocumentError) } defer func() { // errs holds all errors and warnings, // warnings only warnings errs.MergeAsWarnings(warnings) warnings.AddErrors(errs.Warnings...) }() // Swagger schema validator schv := newSchemaValidator(s.schema, nil, "", s.KnownFormats, s.schemaOptions) errs.Merge(schv.Validate(obj)) // error - // There may be a point in continuing to try and determine more accurate errors if !s.Options.ContinueOnErrors && errs.HasErrors() { return errs, warnings // no point in continuing } errs.Merge(s.validateReferencesValid()) // error - // There may be a point in continuing to try and determine more accurate errors if !s.Options.ContinueOnErrors && errs.HasErrors() { return errs, warnings // no point in continuing } errs.Merge(s.validateDuplicateOperationIDs()) errs.Merge(s.validateDuplicatePropertyNames()) // error - errs.Merge(s.validateParameters()) // error - errs.Merge(s.validateItems()) // error - // Properties in required definition MUST validate their schema // Properties SHOULD NOT be declared as both required and readOnly (warning) errs.Merge(s.validateRequiredDefinitions()) // error and warning // There may be a point in continuing to try and determine more accurate errors if !s.Options.ContinueOnErrors && errs.HasErrors() { return errs, warnings // no point in continuing } // Values provided as default MUST validate their schema df := &defaultValidator{SpecValidator: s, schemaOptions: s.schemaOptions} errs.Merge(df.Validate()) // Values provided as examples MUST validate their schema // Value provided as examples in a response without schema generate a warning // Known limitations: examples in responses for mime type not application/json are ignored (warning) ex := &exampleValidator{SpecValidator: s, schemaOptions: s.schemaOptions} errs.Merge(ex.Validate()) errs.Merge(s.validateNonEmptyPathParamNames()) // errs.Merge(s.validateRefNoSibling()) // warning only errs.Merge(s.validateReferenced()) // warning only return errs, warnings } func (s *SpecValidator) validateNonEmptyPathParamNames() *Result { res := pools.poolOfResults.BorrowResult() if s.spec.Spec().Paths == nil { // There is no Paths object: error res.AddErrors(noValidPathMsg()) return res } if s.spec.Spec().Paths.Paths == nil { // Paths may be empty: warning res.AddWarnings(noValidPathMsg()) return res } for k := range s.spec.Spec().Paths.Paths { if strings.Contains(k, "{}") { res.AddErrors(emptyPathParameterMsg(k)) } } return res } func (s *SpecValidator) validateDuplicateOperationIDs() *Result { // OperationID, if specified, must be unique across the board var analyzer *analysis.Spec if s.expanded != nil { // $ref are valid: we can analyze operations on an expanded spec analyzer = analysis.New(s.expanded.Spec()) } else { // fallback on possible incomplete picture because of previous errors analyzer = s.analyzer } res := pools.poolOfResults.BorrowResult() known := make(map[string]int) for _, v := range analyzer.OperationIDs() { if v != "" { known[v]++ } } for k, v := range known { if v > 1 { res.AddErrors(nonUniqueOperationIDMsg(k, v)) } } return res } type dupProp struct { Name string Definition string } func (s *SpecValidator) validateDuplicatePropertyNames() *Result { // definition can't declare a property that's already defined by one of its ancestors res := pools.poolOfResults.BorrowResult() for k, sch := range s.spec.Spec().Definitions { if len(sch.AllOf) == 0 { continue } knownanc := map[string]struct{}{ "#/definitions/" + k: {}, } ancs, rec := s.validateCircularAncestry(k, sch, knownanc) if rec != nil && (rec.HasErrors() || !rec.HasWarnings()) { res.Merge(rec) } if len(ancs) > 0 { res.AddErrors(circularAncestryDefinitionMsg(k, ancs)) return res } knowns := make(map[string]struct{}) dups, rep := s.validateSchemaPropertyNames(k, sch, knowns) if rep != nil && (rep.HasErrors() || rep.HasWarnings()) { res.Merge(rep) } if len(dups) > 0 { var pns []string for _, v := range dups { pns = append(pns, v.Definition+"."+v.Name) } res.AddErrors(duplicatePropertiesMsg(k, pns)) } } return res } func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) { if s.spec.SpecFilePath() != "" { return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()}) } // NOTE: it looks like with the new spec resolver, this code is now unrecheable return spec.ResolveRef(s.spec.Spec(), ref) } func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) ([]dupProp, *Result) { var dups []dupProp schn := nm schc := &sch res := pools.poolOfResults.BorrowResult() for schc.Ref.String() != "" { // gather property names reso, err := s.resolveRef(&schc.Ref) if err != nil { errorHelp.addPointerError(res, err, schc.Ref.String(), nm) return dups, res } schc = reso schn = sch.Ref.String() } if len(schc.AllOf) > 0 { for _, chld := range schc.AllOf { dup, rep := s.validateSchemaPropertyNames(schn, chld, knowns) if rep != nil && (rep.HasErrors() || rep.HasWarnings()) { res.Merge(rep) } dups = append(dups, dup...) } return dups, res } for k := range schc.Properties { _, ok := knowns[k] if ok { dups = append(dups, dupProp{Name: k, Definition: schn}) } else { knowns[k] = struct{}{} } } return dups, res } func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, knowns map[string]struct{}) ([]string, *Result) { res := pools.poolOfResults.BorrowResult() if sch.Ref.String() == "" && len(sch.AllOf) == 0 { // Safeguard. We should not be able to actually get there return nil, res } var ancs []string schn := nm schc := &sch for schc.Ref.String() != "" { reso, err := s.resolveRef(&schc.Ref) if err != nil { errorHelp.addPointerError(res, err, schc.Ref.String(), nm) return ancs, res } schc = reso schn = sch.Ref.String() } if schn != nm && schn != "" { if _, ok := knowns[schn]; ok { ancs = append(ancs, schn) } knowns[schn] = struct{}{} if len(ancs) > 0 { return ancs, res } } if len(schc.AllOf) > 0 { for _, chld := range schc.AllOf { if chld.Ref.String() != "" || len(chld.AllOf) > 0 { anc, rec := s.validateCircularAncestry(schn, chld, knowns) if rec != nil && (rec.HasErrors() || !rec.HasWarnings()) { res.Merge(rec) } ancs = append(ancs, anc...) if len(ancs) > 0 { return ancs, res } } } } return ancs, res } func (s *SpecValidator) validateItems() *Result { // validate parameter, items, schema and response objects for presence of item if type is array res := pools.poolOfResults.BorrowResult() for method, pi := range s.analyzer.Operations() { for path, op := range pi { for _, param := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) { if param.TypeName() == arrayType && param.ItemsTypeName() == "" { res.AddErrors(arrayInParamRequiresItemsMsg(param.Name, op.ID)) continue } if param.In != swaggerBody { if param.Items != nil { items := param.Items for items.TypeName() == arrayType { if items.ItemsTypeName() == "" { res.AddErrors(arrayInParamRequiresItemsMsg(param.Name, op.ID)) break } items = items.Items } } } else { // In: body if param.Schema != nil { res.Merge(s.validateSchemaItems(*param.Schema, fmt.Sprintf("body param %q", param.Name), op.ID)) } } } var responses []spec.Response if op.Responses != nil { if op.Responses.Default != nil { responses = append(responses, *op.Responses.Default) } if op.Responses.StatusCodeResponses != nil { for _, v := range op.Responses.StatusCodeResponses { responses = append(responses, v) } } } for _, resp := range responses { // Response headers with array for hn, hv := range resp.Headers { if hv.TypeName() == arrayType && hv.ItemsTypeName() == "" { res.AddErrors(arrayInHeaderRequiresItemsMsg(hn, op.ID)) } } if resp.Schema != nil { res.Merge(s.validateSchemaItems(*resp.Schema, "response body", op.ID)) } } } } return res } // Verifies constraints on array type func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID string) *Result { res := pools.poolOfResults.BorrowResult() if !schema.Type.Contains(arrayType) { return res } if schema.Items == nil || schema.Items.Len() == 0 { res.AddErrors(arrayRequiresItemsMsg(prefix, opID)) return res } if schema.Items.Schema != nil { schema = *schema.Items.Schema if _, err := compileRegexp(schema.Pattern); err != nil { res.AddErrors(invalidItemsPatternMsg(prefix, opID, schema.Pattern)) } res.Merge(s.validateSchemaItems(schema, prefix, opID)) } return res } func (s *SpecValidator) validatePathParamPresence(path string, fromPath, fromOperation []string) *Result { // Each defined operation path parameters must correspond to a named element in the API's path pattern. // (For example, you cannot have a path parameter named id for the following path /pets/{petId} but you must have a path parameter named petId.) res := pools.poolOfResults.BorrowResult() for _, l := range fromPath { var matched bool for _, r := range fromOperation { if l == "{"+r+"}" { matched = true break } } if !matched { res.AddErrors(noParameterInPathMsg(l)) } } for _, p := range fromOperation { var matched bool for _, r := range fromPath { if "{"+p+"}" == r { matched = true break } } if !matched { res.AddErrors(pathParamNotInPathMsg(path, p)) } } return res } func (s *SpecValidator) validateReferenced() *Result { var res Result res.MergeAsWarnings(s.validateReferencedParameters()) res.MergeAsWarnings(s.validateReferencedResponses()) res.MergeAsWarnings(s.validateReferencedDefinitions()) return &res } func (s *SpecValidator) validateReferencedParameters() *Result { // Each referenceable definition should have references. params := s.spec.Spec().Parameters if len(params) == 0 { return nil } expected := make(map[string]struct{}) for k := range params { expected["#/parameters/"+jsonpointer.Escape(k)] = struct{}{} } for _, k := range s.analyzer.AllParameterReferences() { delete(expected, k) } if len(expected) == 0 { return nil } result := pools.poolOfResults.BorrowResult() for k := range expected { result.AddWarnings(unusedParamMsg(k)) } return result } func (s *SpecValidator) validateReferencedResponses() *Result { // Each referenceable definition should have references. responses := s.spec.Spec().Responses if len(responses) == 0 { return nil } expected := make(map[string]struct{}) for k := range responses { expected["#/responses/"+jsonpointer.Escape(k)] = struct{}{} } for _, k := range s.analyzer.AllResponseReferences() { delete(expected, k) } if len(expected) == 0 { return nil } result := pools.poolOfResults.BorrowResult() for k := range expected { result.AddWarnings(unusedResponseMsg(k)) } return result } func (s *SpecValidator) validateReferencedDefinitions() *Result { // Each referenceable definition must have references. defs := s.spec.Spec().Definitions if len(defs) == 0 { return nil } expected := make(map[string]struct{}) for k := range defs { expected["#/definitions/"+jsonpointer.Escape(k)] = struct{}{} } for _, k := range s.analyzer.AllDefinitionReferences() { delete(expected, k) } if len(expected) == 0 { return nil } result := new(Result) for k := range expected { result.AddWarnings(unusedDefinitionMsg(k)) } return result } func (s *SpecValidator) validateRequiredDefinitions() *Result { // Each property listed in the required array must be defined in the properties of the model res := pools.poolOfResults.BorrowResult() DEFINITIONS: for d, schema := range s.spec.Spec().Definitions { if schema.Required != nil { // Safeguard for _, pn := range schema.Required { red := s.validateRequiredProperties(pn, d, &schema) //#nosec res.Merge(red) if !red.IsValid() && !s.Options.ContinueOnErrors { break DEFINITIONS // there is an error, let's stop that bleeding } } } } return res } func (s *SpecValidator) validateRequiredProperties(path, in string, v *spec.Schema) *Result { // Takes care of recursive property definitions, which may be nested in additionalProperties schemas res := pools.poolOfResults.BorrowResult() propertyMatch := false patternMatch := false additionalPropertiesMatch := false isReadOnly := false // Regular properties if _, ok := v.Properties[path]; ok { propertyMatch = true isReadOnly = v.Properties[path].ReadOnly } // NOTE: patternProperties are not supported in swagger. Even though, we continue validation here // We check all defined patterns: if one regexp is invalid, croaks an error for pp, pv := range v.PatternProperties { re, err := compileRegexp(pp) if err != nil { res.AddErrors(invalidPatternMsg(pp, in)) } else if re.MatchString(path) { patternMatch = true if !propertyMatch { isReadOnly = pv.ReadOnly } } } if !(propertyMatch || patternMatch) { if v.AdditionalProperties != nil { if v.AdditionalProperties.Allows && v.AdditionalProperties.Schema == nil { additionalPropertiesMatch = true } else if v.AdditionalProperties.Schema != nil { // additionalProperties as schema are upported in swagger // recursively validates additionalProperties schema // TODO : anyOf, allOf, oneOf like in schemaPropsValidator red := s.validateRequiredProperties(path, in, v.AdditionalProperties.Schema) if red.IsValid() { additionalPropertiesMatch = true if !propertyMatch && !patternMatch { isReadOnly = v.AdditionalProperties.Schema.ReadOnly } } res.Merge(red) } } } if !(propertyMatch || patternMatch || additionalPropertiesMatch) { res.AddErrors(requiredButNotDefinedMsg(path, in)) } if isReadOnly { res.AddWarnings(readOnlyAndRequiredMsg(in, path)) } return res } func (s *SpecValidator) validateParameters() *Result { // - for each method, path is unique, regardless of path parameters // e.g. GET:/petstore/{id}, GET:/petstore/{pet}, GET:/petstore are // considered duplicate paths, if StrictPathParamUniqueness is enabled. // - each parameter should have a unique `name` and `type` combination // - each operation should have only 1 parameter of type body // - there must be at most 1 parameter in body // - parameters with pattern property must specify valid patterns // - $ref in parameters must resolve // - path param must be required res := pools.poolOfResults.BorrowResult() rexGarbledPathSegment := mustCompileRegexp(`.*[{}\s]+.*`) for method, pi := range s.expandedAnalyzer().Operations() { methodPaths := make(map[string]map[string]string) for path, op := range pi { if s.Options.StrictPathParamUniqueness { pathToAdd := pathHelp.stripParametersInPath(path) // Warn on garbled path afer param stripping if rexGarbledPathSegment.MatchString(pathToAdd) { res.AddWarnings(pathStrippedParamGarbledMsg(pathToAdd)) } // Check uniqueness of stripped paths if _, found := methodPaths[method][pathToAdd]; found { // Sort names for stable, testable output if strings.Compare(path, methodPaths[method][pathToAdd]) < 0 { res.AddErrors(pathOverlapMsg(path, methodPaths[method][pathToAdd])) } else { res.AddErrors(pathOverlapMsg(methodPaths[method][pathToAdd], path)) } } else { if _, found := methodPaths[method]; !found { methodPaths[method] = map[string]string{} } methodPaths[method][pathToAdd] = path // Original non stripped path } } var bodyParams []string var paramNames []string var hasForm, hasBody bool // Check parameters names uniqueness for operation // TODO: should be done after param expansion res.Merge(s.checkUniqueParams(path, method, op)) // pick the root schema from the swagger specification which describes a parameter origSchema, ok := s.schema.Definitions["parameter"] if !ok { panic("unexpected swagger schema: missing #/definitions/parameter") } // clone it once to avoid expanding a global schema (e.g. swagger spec) paramSchema, err := deepCloneSchema(origSchema) if err != nil { panic(fmt.Errorf("can't clone schema: %v", err)) } for _, pr := range paramHelp.safeExpandedParamsFor(path, method, op.ID, res, s) { // An expanded parameter must validate the Parameter schema (an unexpanded $ref always passes high-level schema validation) schv := newSchemaValidator(¶mSchema, s.schema, fmt.Sprintf("%s.%s.parameters.%s", path, method, pr.Name), s.KnownFormats, s.schemaOptions) obj := swag.ToDynamicJSON(pr) res.Merge(schv.Validate(obj)) // Validate pattern regexp for parameters with a Pattern property if _, err := compileRegexp(pr.Pattern); err != nil { res.AddErrors(invalidPatternInParamMsg(op.ID, pr.Name, pr.Pattern)) } // There must be at most one parameter in body: list them all if pr.In == swaggerBody { bodyParams = append(bodyParams, fmt.Sprintf("%q", pr.Name)) hasBody = true } if pr.In == "path" { paramNames = append(paramNames, pr.Name) // Path declared in path must have the required: true property if !pr.Required { res.AddErrors(pathParamRequiredMsg(op.ID, pr.Name)) } } if pr.In == "formData" { hasForm = true } if !(pr.Type == numberType || pr.Type == integerType) && (pr.Maximum != nil || pr.Minimum != nil || pr.MultipleOf != nil) { // A non-numeric parameter has validation keywords for numeric instances (number and integer) res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) } if !(pr.Type == stringType) && // A non-string parameter has validation keywords for strings (pr.MaxLength != nil || pr.MinLength != nil || pr.Pattern != "") { res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) } if !(pr.Type == arrayType) && // A non-array parameter has validation keywords for arrays (pr.MaxItems != nil || pr.MinItems != nil || pr.UniqueItems) { res.AddWarnings(parameterValidationTypeMismatchMsg(pr.Name, path, pr.Type)) } } // In:formData and In:body are mutually exclusive if hasBody && hasForm { res.AddErrors(bothFormDataAndBodyMsg(op.ID)) } // There must be at most one body param // Accurately report situations when more than 1 body param is declared (possibly unnamed) if len(bodyParams) > 1 { sort.Strings(bodyParams) res.AddErrors(multipleBodyParamMsg(op.ID, bodyParams)) } // Check uniqueness of parameters in path paramsInPath := pathHelp.extractPathParams(path) for i, p := range paramsInPath { for j, q := range paramsInPath { if p == q && i > j { res.AddErrors(pathParamNotUniqueMsg(path, p, q)) break } } } // Warns about possible malformed params in path rexGarbledParam := mustCompileRegexp(`{.*[{}\s]+.*}`) for _, p := range paramsInPath { if rexGarbledParam.MatchString(p) { res.AddWarnings(pathParamGarbledMsg(path, p)) } } // Match params from path vs params from params section res.Merge(s.validatePathParamPresence(path, paramsInPath, paramNames)) } } return res } func (s *SpecValidator) validateReferencesValid() *Result { // each reference must point to a valid object res := pools.poolOfResults.BorrowResult() for _, r := range s.analyzer.AllRefs() { if !r.IsValidURI(s.spec.SpecFilePath()) { // Safeguard - spec should always yield a valid URI res.AddErrors(invalidRefMsg(r.String())) } } if !res.HasErrors() { // NOTE: with default settings, loads.Document.Expanded() // stops on first error. Anyhow, the expand option to continue // on errors fails to report errors at all. exp, err := s.spec.Expanded() if err != nil { res.AddErrors(unresolvedReferencesMsg(err)) } s.expanded = exp } return res } func (s *SpecValidator) checkUniqueParams(path, method string, op *spec.Operation) *Result { // Check for duplicate parameters declaration in param section. // Each parameter should have a unique `name` and `type` combination // NOTE: this could be factorized in analysis (when constructing the params map) // However, there are some issues with such a factorization: // - analysis does not seem to fully expand params // - param keys may be altered by x-go-name res := pools.poolOfResults.BorrowResult() pnames := make(map[string]struct{}) if op.Parameters != nil { // Safeguard for _, ppr := range op.Parameters { var ok bool pr, red := paramHelp.resolveParam(path, method, op.ID, &ppr, s) //#nosec res.Merge(red) if pr != nil && pr.Name != "" { // params with empty name does no participate the check key := fmt.Sprintf("%s#%s", pr.In, pr.Name) if _, ok = pnames[key]; ok { res.AddErrors(duplicateParamNameMsg(pr.In, pr.Name, op.ID)) } pnames[key] = struct{}{} } } } return res } // SetContinueOnErrors sets the ContinueOnErrors option for this validator. func (s *SpecValidator) SetContinueOnErrors(c bool) { s.Options.ContinueOnErrors = c } // expandedAnalyzer returns expanded.Analyzer when it is available. // otherwise just analyzer. func (s *SpecValidator) expandedAnalyzer() *analysis.Spec { if s.expanded != nil && s.expanded.Analyzer != nil { return s.expanded.Analyzer } return s.analyzer } func deepCloneSchema(src spec.Schema) (spec.Schema, error) { var b bytes.Buffer if err := gob.NewEncoder(&b).Encode(src); err != nil { return spec.Schema{}, err } var dst spec.Schema if err := gob.NewDecoder(&b).Decode(&dst); err != nil { return spec.Schema{}, err } return dst, nil }