From 19556cfcc22281022f2a8fa4f5c6ed3290f8025f Mon Sep 17 00:00:00 2001 From: Katsumi Kato Date: Tue, 3 Jan 2023 08:50:57 +0900 Subject: [PATCH] openapi3filter: support for allOf request schema in multipart/form-data (#729) fix https://github.com/getkin/kin-openapi/issues/722 --- openapi3filter/issue722_test.go | 133 +++++++++++++++++++++++++++++ openapi3filter/req_resp_decoder.go | 78 +++++++++++------ 2 files changed, 186 insertions(+), 25 deletions(-) create mode 100644 openapi3filter/issue722_test.go diff --git a/openapi3filter/issue722_test.go b/openapi3filter/issue722_test.go new file mode 100644 index 000000000..2ffa9d143 --- /dev/null +++ b/openapi3filter/issue722_test.go @@ -0,0 +1,133 @@ +package openapi3filter_test + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestValidateMultipartFormDataContainingAllOf(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /test: + post: + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + allOf: + - $ref: '#/components/schemas/Category' + - properties: + file: + type: string + format: binary + description: + type: string + responses: + '200': + description: Created + +components: + schemas: + Category: + type: object + properties: + name: + type: string + required: + - name +` + + loader := openapi3.NewLoader() + doc, err := loader.LoadFromData([]byte(spec)) + if err != nil { + t.Fatal(err) + } + if err = doc.Validate(loader.Context); err != nil { + t.Fatal(err) + } + + router, err := gorillamux.NewRouter(doc) + if err != nil { + t.Fatal(err) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + { // Add file data + fw, err := writer.CreateFormFile("file", "hello.txt") + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader("hello")); err != nil { + t.Fatal(err) + } + } + + { // Add a single "name" item as part data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="name"`) + fw, err := writer.CreatePart(h) + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader(`foo`)); err != nil { + t.Fatal(err) + } + } + + { // Add a single "discription" item as part data + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="description"`) + fw, err := writer.CreatePart(h) + if err != nil { + t.Fatal(err) + } + if _, err = io.Copy(fw, strings.NewReader(`description note`)); err != nil { + t.Fatal(err) + } + } + + writer.Close() + + req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader(body.Bytes())) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + route, pathParams, err := router.FindRoute(req) + if err != nil { + t.Fatal(err) + } + + if err = openapi3filter.ValidateRequestBody( + context.Background(), + &openapi3filter.RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + }, + route.Operation.RequestBody.Value, + ); err != nil { + t.Error(err) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 4791b4ad4..2cd700cd1 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1120,33 +1120,47 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S enc = encFn(name) } subEncFn := func(string) *openapi3.Encoding { return enc } - // If the property's schema has type "array" it is means that the form contains a few parts with the same name. - // Every such part has a type that is defined by an items schema in the property's schema. + var valueSchema *openapi3.SchemaRef - var exists bool - valueSchema, exists = schema.Value.Properties[name] - if !exists { - anyProperties := schema.Value.AdditionalPropertiesAllowed - if anyProperties != nil { - switch *anyProperties { - case true: - //additionalProperties: true - continue - default: - //additionalProperties: false - return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + if len(schema.Value.AllOf) > 0 { + var exists bool + for _, sr := range schema.Value.AllOf { + valueSchema, exists = sr.Value.Properties[name] + if exists { + break } } - if schema.Value.AdditionalProperties == nil { + if !exists { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } - valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] + } else { + // If the property's schema has type "array" it is means that the form contains a few parts with the same name. + // Every such part has a type that is defined by an items schema in the property's schema. + var exists bool + valueSchema, exists = schema.Value.Properties[name] if !exists { - return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + anyProperties := schema.Value.AdditionalPropertiesAllowed + if anyProperties != nil { + switch *anyProperties { + case true: + //additionalProperties: true + continue + default: + //additionalProperties: false + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + } + if schema.Value.AdditionalProperties == nil { + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] + if !exists { + return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} + } + } + if valueSchema.Value.Type == "array" { + valueSchema = valueSchema.Value.Items } - } - if valueSchema.Value.Type == "array" { - valueSchema = valueSchema.Value.Items } var value interface{} @@ -1160,14 +1174,28 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } allTheProperties := make(map[string]*openapi3.SchemaRef) - for k, v := range schema.Value.Properties { - allTheProperties[k] = v - } - if schema.Value.AdditionalProperties != nil { - for k, v := range schema.Value.AdditionalProperties.Value.Properties { + if len(schema.Value.AllOf) > 0 { + for _, sr := range schema.Value.AllOf { + for k, v := range sr.Value.Properties { + allTheProperties[k] = v + } + if sr.Value.AdditionalProperties != nil { + for k, v := range sr.Value.AdditionalProperties.Value.Properties { + allTheProperties[k] = v + } + } + } + } else { + for k, v := range schema.Value.Properties { allTheProperties[k] = v } + if schema.Value.AdditionalProperties != nil { + for k, v := range schema.Value.AdditionalProperties.Value.Properties { + allTheProperties[k] = v + } + } } + // Make an object value from form values. obj := make(map[string]interface{}) for name, prop := range allTheProperties {