k3s/vendor/github.com/go-openapi/validate/spec.go

793 lines
22 KiB
Go

// 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 (
"encoding/json"
"fmt"
"log"
"regexp"
"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"
)
// Spec validates a spec document
// It validates the spec json against the json schema for swagger
// and then validates a number of extra rules that can't be expressed in json schema:
//
// - definition can't declare a property that's already defined by one of its ancestors
// - definition's ancestor can't be a descendant of the same model
// - each api path should be non-verbatim (account for path param names) unique per method
// - each security reference should contain only unique scopes
// - each security scope in a security definition should be unique
// - each path parameter should correspond to a parameter placeholder and vice versa
// - each referencable definition must have references
// - each definition property listed in the required array must be defined in the properties of the model
// - each parameter should have a unique `name` and `type` combination
// - each operation should have only 1 parameter of type body
// - each reference must point to a valid object
// - every default value that is specified must validate against the schema for that property
// - items property is required for all schemas/definitions of type `array`
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
}
// AgainstSchema validates the specified data with the provided schema, when no schema
// is provided it uses the json schema as default
func AgainstSchema(schema *spec.Schema, data interface{}, formats strfmt.Registry) error {
res := NewSchemaValidator(schema, nil, "", formats).Validate(data)
if res.HasErrors() {
return errors.CompositeValidationError(res.Errors...)
}
return nil
}
// SpecValidator validates a swagger spec
type SpecValidator struct {
schema *spec.Schema // swagger 2.0 schema
spec *loads.Document
analyzer *analysis.Spec
expanded *loads.Document
KnownFormats strfmt.Registry
}
// NewSpecValidator creates a new swagger spec validator instance
func NewSpecValidator(schema *spec.Schema, formats strfmt.Registry) *SpecValidator {
return &SpecValidator{
schema: schema,
KnownFormats: formats,
}
}
// Validate validates the swagger spec
func (s *SpecValidator) Validate(data interface{}) (errs *Result, warnings *Result) {
var sd *loads.Document
switch v := data.(type) {
case *loads.Document:
sd = v
}
if sd == nil {
errs = sErr(errors.New(500, "spec validator can only validate spec.Document objects"))
return
}
s.spec = sd
s.analyzer = analysis.New(sd.Spec())
errs = new(Result)
warnings = new(Result)
schv := NewSchemaValidator(s.schema, nil, "", s.KnownFormats)
var obj interface{}
if err := json.Unmarshal(sd.Raw(), &obj); err != nil {
errs.AddErrors(err)
return
}
errs.Merge(schv.Validate(obj)) // error -
if errs.HasErrors() {
return // no point in continuing
}
errs.Merge(s.validateReferencesValid()) // error -
if errs.HasErrors() {
return // no point in continuing
}
errs.Merge(s.validateDuplicateOperationIDs())
errs.Merge(s.validateDuplicatePropertyNames()) // error -
errs.Merge(s.validateParameters()) // error -
errs.Merge(s.validateItems()) // error -
errs.Merge(s.validateRequiredDefinitions()) // error -
errs.Merge(s.validateDefaultValueValidAgainstSchema()) // error -
errs.Merge(s.validateExamplesValidAgainstSchema()) // error -
errs.Merge(s.validateNonEmptyPathParamNames())
warnings.Merge(s.validateUniqueSecurityScopes()) // warning
warnings.Merge(s.validateReferenced()) // warning
return
}
func (s *SpecValidator) validateNonEmptyPathParamNames() *Result {
res := new(Result)
for k := range s.spec.Spec().Paths.Paths {
if strings.Contains(k, "{}") {
res.AddErrors(errors.New(422, "%q contains an empty path parameter", k))
}
}
return res
}
func (s *SpecValidator) validateDuplicateOperationIDs() *Result {
res := new(Result)
known := make(map[string]int)
for _, v := range s.analyzer.OperationIDs() {
if v != "" {
known[v]++
}
}
for k, v := range known {
if v > 1 {
res.AddErrors(errors.New(422, "%q is defined %d times", 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 := new(Result)
for k, sch := range s.spec.Spec().Definitions {
if len(sch.AllOf) == 0 {
continue
}
knownanc := map[string]struct{}{
"#/definitions/" + k: struct{}{},
}
ancs := s.validateCircularAncestry(k, sch, knownanc)
if len(ancs) > 0 {
res.AddErrors(errors.New(422, "definition %q has circular ancestry: %v", k, ancs))
return res
}
knowns := make(map[string]struct{})
dups := s.validateSchemaPropertyNames(k, sch, knowns)
if len(dups) > 0 {
var pns []string
for _, v := range dups {
pns = append(pns, v.Definition+"."+v.Name)
}
res.AddErrors(errors.New(422, "definition %q contains duplicate properties: %v", k, pns))
}
}
return res
}
func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp {
var dups []dupProp
schn := nm
schc := &sch
for schc.Ref.String() != "" {
// gather property names
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
if err != nil {
panic(err)
}
schc = reso
schn = sch.Ref.String()
}
if len(schc.AllOf) > 0 {
for _, chld := range schc.AllOf {
dups = append(dups, s.validateSchemaPropertyNames(schn, chld, knowns)...)
}
return dups
}
for k := range schc.Properties {
_, ok := knowns[k]
if ok {
dups = append(dups, dupProp{Name: k, Definition: schn})
} else {
knowns[k] = struct{}{}
}
}
return dups
}
func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, knowns map[string]struct{}) []string {
if sch.Ref.String() == "" && len(sch.AllOf) == 0 {
return nil
}
var ancs []string
schn := nm
schc := &sch
for schc.Ref.String() != "" {
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
if err != nil {
panic(err)
}
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
}
}
if len(schc.AllOf) > 0 {
for _, chld := range schc.AllOf {
if chld.Ref.String() != "" || len(chld.AllOf) > 0 {
ancs = append(ancs, s.validateCircularAncestry(schn, chld, knowns)...)
if len(ancs) > 0 {
return ancs
}
}
}
}
return ancs
}
func (s *SpecValidator) validateItems() *Result {
// validate parameter, items, schema and response objects for presence of item if type is array
res := new(Result)
// TODO: implement support for lookups of refs
for method, pi := range s.analyzer.Operations() {
for path, op := range pi {
for _, param := range s.analyzer.ParamsFor(method, path) {
if param.TypeName() == "array" && param.ItemsTypeName() == "" {
res.AddErrors(errors.New(422, "param %q for %q is a collection without an element type", param.Name, op.ID))
continue
}
if param.In != "body" {
if param.Items != nil {
items := param.Items
for items.TypeName() == "array" {
if items.ItemsTypeName() == "" {
res.AddErrors(errors.New(422, "param %q for %q is a collection without an element type", param.Name, op.ID))
break
}
items = items.Items
}
}
} else {
if err := s.validateSchemaItems(*param.Schema, fmt.Sprintf("body param %q", param.Name), op.ID); err != nil {
res.AddErrors(err)
}
}
}
var responses []spec.Response
if op.Responses != nil {
if op.Responses.Default != nil {
responses = append(responses, *op.Responses.Default)
}
for _, v := range op.Responses.StatusCodeResponses {
responses = append(responses, v)
}
}
for _, resp := range responses {
for hn, hv := range resp.Headers {
if hv.TypeName() == "array" && hv.ItemsTypeName() == "" {
res.AddErrors(errors.New(422, "header %q for %q is a collection without an element type", hn, op.ID))
}
}
if resp.Schema != nil {
if err := s.validateSchemaItems(*resp.Schema, "response body", op.ID); err != nil {
res.AddErrors(err)
}
}
}
}
}
return res
}
func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID string) error {
if !schema.Type.Contains("array") {
return nil
}
if schema.Items == nil || schema.Items.Len() == 0 {
return errors.New(422, "%s for %q is a collection without an element type", prefix, opID)
}
schemas := schema.Items.Schemas
if schema.Items.Schema != nil {
schemas = []spec.Schema{*schema.Items.Schema}
}
for _, sch := range schemas {
if err := s.validateSchemaItems(sch, prefix, opID); err != nil {
return err
}
}
return nil
}
func (s *SpecValidator) validateUniqueSecurityScopes() *Result {
// Each authorization/security reference should contain only unique scopes.
// (Example: For an oauth2 authorization/security requirement, when listing the required scopes,
// each scope should only be listed once.)
return nil
}
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 := new(Result)
for _, l := range fromPath {
var matched bool
for _, r := range fromOperation {
if l == "{"+r+"}" {
matched = true
break
}
}
if !matched {
res.Errors = append(res.Errors, errors.New(422, "path param %q has no parameter definition", l))
}
}
for _, p := range fromOperation {
var matched bool
for _, r := range fromPath {
if "{"+p+"}" == r {
matched = true
break
}
}
if !matched {
res.AddErrors(errors.New(422, "path param %q is not present in path %q", p, path))
}
}
return res
}
func (s *SpecValidator) validateReferenced() *Result {
var res Result
res.Merge(s.validateReferencedParameters())
res.Merge(s.validateReferencedResponses())
res.Merge(s.validateReferencedDefinitions())
return &res
}
func (s *SpecValidator) validateReferencedParameters() *Result {
// Each referenceable definition must 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() {
if _, ok := expected[k]; ok {
delete(expected, k)
}
}
if len(expected) == 0 {
return nil
}
var result Result
for k := range expected {
result.AddErrors(errors.New(422, "parameter %q is not used anywhere", k))
}
return &result
}
func (s *SpecValidator) validateReferencedResponses() *Result {
// Each referenceable definition must 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() {
if _, ok := expected[k]; ok {
delete(expected, k)
}
}
if len(expected) == 0 {
return nil
}
var result Result
for k := range expected {
result.AddErrors(errors.New(422, "response %q is not used anywhere", 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() {
if _, ok := expected[k]; ok {
delete(expected, k)
}
}
if len(expected) == 0 {
return nil
}
var result Result
for k := range expected {
result.AddErrors(errors.New(422, "definition %q is not used anywhere", k))
}
return &result
}
func (s *SpecValidator) validateRequiredDefinitions() *Result {
// Each definition property listed in the required array must be defined in the properties of the model
res := new(Result)
for d, v := range s.spec.Spec().Definitions {
REQUIRED:
for _, pn := range v.Required {
if _, ok := v.Properties[pn]; ok {
continue
}
for pp := range v.PatternProperties {
re := regexp.MustCompile(pp)
if re.MatchString(pn) {
continue REQUIRED
}
}
if v.AdditionalProperties != nil {
if v.AdditionalProperties.Allows {
continue
}
if v.AdditionalProperties.Schema != nil {
continue
}
}
res.AddErrors(errors.New(422, "%q is present in required but not defined as property in definition %q", pn, d))
}
}
return res
}
func (s *SpecValidator) validateParameters() *Result {
// each parameter should have a unique `name` and `type` combination
// each operation should have only 1 parameter of type body
// each api path should be non-verbatim (account for path param names) unique per method
res := new(Result)
for method, pi := range s.analyzer.Operations() {
knownPaths := make(map[string]string)
for path, op := range pi {
segments, params := parsePath(path)
knowns := make([]string, 0, len(segments))
for _, s := range segments {
knowns = append(knowns, s)
}
var fromPath []string
for _, i := range params {
fromPath = append(fromPath, knowns[i])
knowns[i] = "!"
}
knownPath := strings.Join(knowns, "/")
if orig, ok := knownPaths[knownPath]; ok {
res.AddErrors(errors.New(422, "path %s overlaps with %s", path, orig))
} else {
knownPaths[knownPath] = path
}
ptypes := make(map[string]map[string]struct{})
var firstBodyParam string
sw := s.spec.Spec()
var paramNames []string
PARAMETERS:
for _, ppr := range op.Parameters {
pr := ppr
for pr.Ref.String() != "" {
obj, _, err := pr.Ref.GetPointer().Get(sw)
if err != nil {
log.Println(err)
res.AddErrors(err)
break PARAMETERS
}
pr = obj.(spec.Parameter)
}
pnames, ok := ptypes[pr.In]
if !ok {
pnames = make(map[string]struct{})
ptypes[pr.In] = pnames
}
_, ok = pnames[pr.Name]
if ok {
res.AddErrors(errors.New(422, "duplicate parameter name %q for %q in operation %q", pr.Name, pr.In, op.ID))
}
pnames[pr.Name] = struct{}{}
}
PARAMETERS2:
for _, ppr := range s.analyzer.ParamsFor(method, path) {
pr := ppr
for pr.Ref.String() != "" {
obj, _, err := pr.Ref.GetPointer().Get(sw)
if err != nil {
res.AddErrors(err)
break PARAMETERS2
}
pr = obj.(spec.Parameter)
}
if pr.In == "body" {
if firstBodyParam != "" {
res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name))
}
firstBodyParam = pr.Name
}
if pr.In == "path" {
paramNames = append(paramNames, pr.Name)
}
}
res.Merge(s.validatePathParamPresence(path, fromPath, paramNames))
}
}
return res
}
func parsePath(path string) (segments []string, params []int) {
for i, p := range strings.Split(path, "/") {
segments = append(segments, p)
if len(p) > 0 && p[0] == '{' && p[len(p)-1] == '}' {
params = append(params, i)
}
}
return
}
func (s *SpecValidator) validateReferencesValid() *Result {
// each reference must point to a valid object
res := new(Result)
for _, r := range s.analyzer.AllRefs() {
if !r.IsValidURI() {
res.AddErrors(errors.New(404, "invalid ref %q", r.String()))
}
}
if !res.HasErrors() {
exp, err := s.spec.Expanded()
if err != nil {
res.AddErrors(err)
}
s.expanded = exp
}
return res
}
func (s *SpecValidator) validateResponseExample(path string, r *spec.Response) *Result {
res := new(Result)
if r.Ref.String() != "" {
nr, _, err := r.Ref.GetPointer().Get(s.spec.Spec())
if err != nil {
res.AddErrors(err)
return res
}
rr := nr.(spec.Response)
return s.validateResponseExample(path, &rr)
}
if r.Examples != nil {
if r.Schema != nil {
if example, ok := r.Examples["application/json"]; ok {
res.Merge(NewSchemaValidator(r.Schema, s.spec.Spec(), path, s.KnownFormats).Validate(example))
}
// TODO: validate other media types too
}
}
return res
}
func (s *SpecValidator) validateExamplesValidAgainstSchema() *Result {
res := new(Result)
for _, pathItem := range s.analyzer.Operations() {
for path, op := range pathItem {
if op.Responses.Default != nil {
dr := op.Responses.Default
res.Merge(s.validateResponseExample(path, dr))
}
for _, r := range op.Responses.StatusCodeResponses {
res.Merge(s.validateResponseExample(path, &r))
}
}
}
return res
}
func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
// every default value that is specified must validate against the schema for that property
// headers, items, parameters, schema
res := new(Result)
for method, pathItem := range s.analyzer.Operations() {
for path, op := range pathItem {
// parameters
var hasForm, hasBody bool
PARAMETERS:
for _, pr := range s.analyzer.ParamsFor(method, path) {
// expand ref is necessary
param := pr
for param.Ref.String() != "" {
obj, _, err := param.Ref.GetPointer().Get(s.spec.Spec())
if err != nil {
res.AddErrors(err)
break PARAMETERS
}
param = obj.(spec.Parameter)
}
if param.In == "formData" {
if hasBody && !hasForm {
res.AddErrors(errors.New(422, "operation %q has both formData and body parameters", op.ID))
}
hasForm = true
}
if param.In == "body" {
if hasForm && !hasBody {
res.AddErrors(errors.New(422, "operation %q has both body and formData parameters", op.ID))
}
hasBody = true
}
// check simple paramters first
if param.Default != nil && param.Schema == nil {
//fmt.Println(param.Name, "in", param.In, "has a default without a schema")
// check param valid
res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default))
}
if param.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(param.Name, param.In, &param, param.Items))
}
if param.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(param.Name, param.In, param.Schema))
}
}
if op.Responses.Default != nil {
dr := op.Responses.Default
for nm, h := range dr.Headers {
if h.Default != nil {
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
}
if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
}
}
}
for _, r := range op.Responses.StatusCodeResponses {
for nm, h := range r.Headers {
if h.Default != nil {
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
}
if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
}
}
}
}
}
for nm, sch := range s.spec.Spec().Definitions {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("definitions.%s", nm), "body", &sch))
}
return res
}
func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string, schema *spec.Schema) *Result {
res := new(Result)
if schema != nil {
if schema.Default != nil {
res.Merge(NewSchemaValidator(schema, s.spec.Spec(), path, s.KnownFormats).Validate(schema.Default))
}
if schema.Items != nil {
if schema.Items.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(path+".items", in, schema.Items.Schema))
}
for i, sch := range schema.Items.Schemas {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch))
}
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
}
for propName, prop := range schema.Properties {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(path+"."+propName, in, &prop))
}
for propName, prop := range schema.PatternProperties {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(path+"."+propName, in, &prop))
}
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalProperties", path), in, schema.AdditionalProperties.Schema))
}
for i, aoSch := range schema.AllOf {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.allOf[%d]", path, i), in, &aoSch))
}
}
return res
}
func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string, root interface{}, items *spec.Items) *Result {
res := new(Result)
if items != nil {
if items.Default != nil {
res.Merge(newItemsValidator(path, in, items, root, s.KnownFormats).Validate(0, items.Default))
}
if items.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items))
}
}
return res
}