Skip to content

Commit 3e15bf5

Browse files
committed
Addressed issues with deepObject validation.
Array composition was not considered and an issue reported in wiretap highlighted that pb33f/wiretap#82 Signed-off-by: Dave Shanley <[email protected]>
1 parent b7d8f80 commit 3e15bf5

File tree

6 files changed

+176
-6
lines changed

6 files changed

+176
-6
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ require (
1818
github.com/mailru/easyjson v0.7.7 // indirect
1919
github.com/pmezard/go-difflib v1.0.0 // indirect
2020
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
21-
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb // indirect
22-
golang.org/x/sync v0.1.0 // indirect
21+
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
22+
golang.org/x/sync v0.6.0 // indirect
2323
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
7777
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
7878
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
7979
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
80+
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
81+
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
8082
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
8183
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
8284
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -91,6 +93,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
9193
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
9294
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
9395
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
96+
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
97+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
9498
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
9599
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
96100
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

helpers/parameter_utilities.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/pb33f/libopenapi/datamodel/high/base"
99
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
1010
"net/http"
11+
"slices"
1112
"strconv"
1213
"strings"
1314
)
@@ -121,16 +122,55 @@ func cast(v string) any {
121122

122123
// ConstructParamMapFromDeepObjectEncoding will construct a map from the query parameters that are encoded as
123124
// deep objects. It's kind of a crazy way to do things, but hey, each to their own.
124-
func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam) map[string]interface{} {
125+
func ConstructParamMapFromDeepObjectEncoding(values []*QueryParam, sch *base.Schema) map[string]interface{} {
125126
// deepObject encoding is a technique used to encode objects into query parameters. Kinda nuts.
126127
decoded := make(map[string]interface{})
127128
for _, v := range values {
128129
if decoded[v.Key] == nil {
130+
129131
props := make(map[string]interface{})
130-
props[v.Property] = cast(v.Values[0])
132+
rawValues := make([]interface{}, len(v.Values))
133+
for i := range v.Values {
134+
rawValues[i] = cast(v.Values[i])
135+
}
136+
// check if the schema for the param is an array
137+
if sch != nil && slices.Contains(sch.Type, Array) {
138+
props[v.Property] = rawValues
139+
}
140+
// check if schema has additional properties defined as an array
141+
if sch != nil && sch.AdditionalProperties != nil &&
142+
sch.AdditionalProperties.IsA() &&
143+
slices.Contains(sch.AdditionalProperties.A.Schema().Type, Array) {
144+
props[v.Property] = rawValues
145+
}
146+
147+
if len(props) == 0 {
148+
props[v.Property] = cast(v.Values[0])
149+
}
131150
decoded[v.Key] = props
132151
} else {
133-
decoded[v.Key].(map[string]interface{})[v.Property] = cast(v.Values[0])
152+
153+
added := false
154+
rawValues := make([]interface{}, len(v.Values))
155+
for i := range v.Values {
156+
rawValues[i] = cast(v.Values[i])
157+
}
158+
// check if the schema for the param is an array
159+
if sch != nil && slices.Contains(sch.Type, Array) {
160+
decoded[v.Key].(map[string]interface{})[v.Property] = rawValues
161+
added = true
162+
}
163+
// check if schema has additional properties defined as an array
164+
if sch != nil && sch.AdditionalProperties != nil &&
165+
sch.AdditionalProperties.IsA() &&
166+
slices.Contains(sch.AdditionalProperties.A.Schema().Type, Array) {
167+
decoded[v.Key].(map[string]interface{})[v.Property] = rawValues
168+
added = true
169+
}
170+
if !added {
171+
decoded[v.Key].(map[string]interface{})[v.Property] = cast(v.Values[0])
172+
}
173+
134174
}
135175
}
136176
return decoded

parameters/query_parameters.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ doneLooking:
158158

159159
switch params[p].Style {
160160
case helpers.DeepObject:
161-
encodedObj = helpers.ConstructParamMapFromDeepObjectEncoding(jk)
161+
encodedObj = helpers.ConstructParamMapFromDeepObjectEncoding(jk, sch)
162162
case helpers.PipeDelimited:
163163
encodedObj = helpers.ConstructParamMapFromPipeEncoding(jk)
164164
case helpers.SpaceDelimited:

parameters/query_parameters_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,3 +2342,112 @@ paths:
23422342
assert.Equal(t, "lemons 'pizza' is defined as an object, "+
23432343
"however it failed to be decoded as an object", errs[0].Reason)
23442344
}
2345+
2346+
// https://github.com/pb33f/wiretap/issues/82
2347+
func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArray(t *testing.T) {
2348+
spec := `openapi: 3.1.0
2349+
paths:
2350+
/anything/queryParams/deepObject/map:
2351+
get:
2352+
operationId: deepObjectQueryParamsMap
2353+
parameters:
2354+
- name: mapArrParam
2355+
in: query
2356+
style: deepObject
2357+
schema:
2358+
type: object
2359+
additionalProperties:
2360+
type: array
2361+
items:
2362+
type: string
2363+
example: { "test": ["test", "test2"], "test2": ["test3", "test4"] }
2364+
responses:
2365+
"200":
2366+
description: OK`
2367+
2368+
doc, _ := libopenapi.NewDocument([]byte(spec))
2369+
2370+
m, _ := doc.BuildV3Model()
2371+
2372+
v := NewParameterValidator(&m.Model)
2373+
2374+
request, _ := http.NewRequest(http.MethodGet,
2375+
"https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=test3&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil)
2376+
2377+
valid, errors := v.ValidateQueryParams(request)
2378+
assert.True(t, valid)
2379+
2380+
assert.Len(t, errors, 0)
2381+
}
2382+
2383+
// https://github.com/pb33f/wiretap/issues/82
2384+
func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArrayTop(t *testing.T) {
2385+
spec := `openapi: 3.1.0
2386+
paths:
2387+
/anything/queryParams/deepObject/map:
2388+
get:
2389+
operationId: deepObjectQueryParamsMap
2390+
parameters:
2391+
- name: mapArrParam
2392+
in: query
2393+
style: deepObject
2394+
schema:
2395+
type: array
2396+
items:
2397+
type: string
2398+
example: { "test": ["test", "test2"], "test2": ["test3", "test4"] }
2399+
responses:
2400+
"200":
2401+
description: OK`
2402+
2403+
doc, _ := libopenapi.NewDocument([]byte(spec))
2404+
2405+
m, _ := doc.BuildV3Model()
2406+
2407+
v := NewParameterValidator(&m.Model)
2408+
2409+
request, _ := http.NewRequest(http.MethodGet,
2410+
"https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=test3&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil)
2411+
2412+
valid, errors := v.ValidateQueryParams(request)
2413+
assert.True(t, valid)
2414+
2415+
assert.Len(t, errors, 0)
2416+
}
2417+
2418+
// https://github.com/pb33f/wiretap/issues/82
2419+
func TestNewValidator_QueryParamValidateStyle_DeepObjectAdditionalPropertiesArray_Fail(t *testing.T) {
2420+
spec := `openapi: 3.1.0
2421+
paths:
2422+
/anything/queryParams/deepObject/map:
2423+
get:
2424+
operationId: deepObjectQueryParamsMap
2425+
parameters:
2426+
- name: mapArrParam
2427+
in: query
2428+
style: deepObject
2429+
schema:
2430+
type: object
2431+
additionalProperties:
2432+
type: array
2433+
items:
2434+
type: string
2435+
example: { "test": ["test", "test2"], "test2": ["test3", "test4"] }
2436+
responses:
2437+
"200":
2438+
description: OK`
2439+
2440+
doc, _ := libopenapi.NewDocument([]byte(spec))
2441+
2442+
m, _ := doc.BuildV3Model()
2443+
2444+
v := NewParameterValidator(&m.Model)
2445+
2446+
request, _ := http.NewRequest(http.MethodGet,
2447+
"https://things.com/anything/queryParams/deepObject/map?mapArrParam[test2]=23&mapArrParam[test2]=test4&mapArrParam[test]=test&mapArrParam[test]=test2", nil)
2448+
2449+
valid, errors := v.ValidateQueryParams(request)
2450+
assert.False(t, valid)
2451+
assert.Len(t, errors, 1)
2452+
assert.Equal(t, "expected string, but got number", errors[0].SchemaValidationErrors[0].Reason)
2453+
}

parameters/validation_functions.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pb33f/libopenapi-validator/helpers"
1010
"github.com/pb33f/libopenapi/datamodel/high/base"
1111
"github.com/pb33f/libopenapi/datamodel/high/v3"
12+
"slices"
1213
"strconv"
1314
"strings"
1415
)
@@ -192,6 +193,22 @@ stopValidation:
192193
for i := range qp.Values {
193194
switch param.Style {
194195
case helpers.DeepObject:
196+
// check if the object has additional properties defined that treat this as an array
197+
if param.Schema != nil {
198+
pSchema := param.Schema.Schema()
199+
if slices.Contains(pSchema.Type, helpers.Array) {
200+
continue
201+
}
202+
if pSchema.AdditionalProperties != nil && pSchema.AdditionalProperties.IsA() {
203+
addPropSchema := pSchema.AdditionalProperties.A.Schema()
204+
if addPropSchema.Type != nil && len(addPropSchema.Type) > 0 {
205+
if slices.Contains(addPropSchema.Type, helpers.Array) {
206+
// an array can have more than one value.
207+
continue
208+
}
209+
}
210+
}
211+
}
195212
if len(qp.Values) > 1 {
196213
validationErrors = append(validationErrors, errors.InvalidDeepObject(param, qp))
197214
break stopValidation

0 commit comments

Comments
 (0)