Repository: swaggest/openapi-go
Pull request #33 (2022-04-13 23:45:09)

Forbid unknown parameters if parent has additionalProperties: false

github-actions[bot]: ### Lines Of Code

| Language  | Files |   Lines    |    Code    | Comments |  Blanks  | Complexity |     Bytes     |
|-----------|-------|------------|------------|----------|----------|------------|---------------|
| Go        | 6     | 1207 (+20) | 905 (+13)  | 55 (+3)  | 247 (+4) | 295 (+6)   | 27.3K (+713B) |
| Go (test) | 5     | 1476 (+42) | 1065 (+37) | 228      | 183 (+5) | 16         | 38K (+1.1K)   |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/openapi-go/openapi3
## compatible changes
Operation.UnknownParamIsForbidden: added

# summary
Inferred base version: v0.2.15
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:	(statements)	48.4%
<details><summary>Coverage diff with base branch</summary>

```diff
284c284
< github.com/swaggest/openapi-go/openapi3/entities.go	WithMapOfAnythingItem	0.0%
---
> github.com/swaggest/openapi-go/openapi3/entities.go	WithMapOfAnythingItem	100.0%
551a552
> github.com/swaggest/openapi-go/openapi3/helper.go	UnknownParamIsForbidden	0.0%
564c565
< github.com/swaggest/openapi-go/openapi3/reflect.go	parseParametersIn	82.6%
---
> github.com/swaggest/openapi-go/openapi3/reflect.go	parseParametersIn	83.3%
578c579
< total:	(statements)	48.3%
---
> total:	(statements)	48.4%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/openapi-go/pull/33?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#33](https://codecov.io/gh/swaggest/openapi-go/pull/33?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (6f9b669) into [master](https://codecov.io/gh/swaggest/openapi-go/commit/8c92e1d715fa052e60e93b87d6687f1189b3be1a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (8c92e1d) will **decrease** coverage by `0.37%`.
> The diff coverage is `60.00%`.

```diff
@@            Coverage Diff             @@
##           master      #33      +/-   ##
==========================================
- Coverage   40.18%   39.81%   -0.38%     
==========================================
  Files           5        5              
  Lines        3511     3456      -55     
==========================================
- Hits         1411     1376      -35     
+ Misses       1806     1795      -11     
+ Partials      294      285       -9     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `39.81% <60.00%> (-0.38%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/openapi-go/pull/33?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [openapi3/helper.go](https://codecov.io/gh/swaggest/openapi-go/pull/33/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvaGVscGVyLmdv) | `83.33% <0.00%> (-3.21%)` | :arrow_down: |
| [openapi3/reflect.go](https://codecov.io/gh/swaggest/openapi-go/pull/33/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `88.33% <100.00%> (+4.78%)` | :arrow_up: |
| [openapi3/entities.go](https://codecov.io/gh/swaggest/openapi-go/pull/33/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvZW50aXRpZXMuZ28=) | `34.30% <0.00%> (+0.13%)` | :arrow_up: |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml
index 7002b22..d6ff20d 100644
--- a/.github/workflows/cloc.yml
+++ b/.github/workflows/cloc.yml
@@ -2,6 +2,12 @@
 name: cloc
 on:
   pull_request:
+
+# Cancel the workflow in progress in newer build is about to start.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
   cloc:
     runs-on: ubuntu-latest
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 58c0ec6..b4f7600 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -8,17 +8,26 @@ on:
       - master
       - main
   pull_request:
+
+# Cancel the workflow in progress in newer build is about to start.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 jobs:
   golangci:
     name: golangci-lint
     runs-on: ubuntu-latest
     steps:
+      - uses: actions/setup-go@v3
+        with:
+          go-version: 1.18.x
       - uses: actions/checkout@v2
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2.5.2
+        uses: golangci/golangci-lint-action@v3.1.0
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
-          version: v1.43.0
+          version: v1.45.2
 
           # Optional: working directory, useful for monorepos
           # working-directory: somedir
diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml
index 94c6966..429d5ed 100644
--- a/.github/workflows/gorelease.yml
+++ b/.github/workflows/gorelease.yml
@@ -2,15 +2,21 @@
 name: gorelease
 on:
   pull_request:
+
+# Cancel the workflow in progress in newer build is about to start.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 env:
-  GO_VERSION: 1.17.x
+  GO_VERSION: 1.18.x
 jobs:
   gorelease:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go stable
         if: env.GO_VERSION != 'tip'
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: ${{ env.GO_VERSION }}
       - name: Install Go tip
diff --git a/.golangci.yml b/.golangci.yml
index 5c86084..4b52e4d 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -57,4 +57,7 @@ issues:
     - linters:
         - errcheck
       path: "example_"
+    - linters:
+        - staticcheck
+      text: "SA1019: strings.Title is deprecated"
 
diff --git a/Makefile b/Makefile
index 4a30096..2788f42 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-#GOLANGCI_LINT_VERSION := "v1.43.0" # Optional configuration to pinpoint golangci-lint version.
+#GOLANGCI_LINT_VERSION := "v1.45.2" # Optional configuration to pinpoint golangci-lint version.
 
 # The head of Makefile determines location of dev-go to include standard targets.
 GO ?= go
diff --git a/go.mod b/go.mod
index fe4a0f7..396f5a9 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module github.com/swaggest/openapi-go
 go 1.17
 
 require (
-	github.com/bool64/dev v0.2.5
-	github.com/stretchr/testify v1.7.0
+	github.com/bool64/dev v0.2.10
+	github.com/stretchr/testify v1.7.1
 	github.com/swaggest/assertjson v1.6.8
-	github.com/swaggest/jsonschema-go v0.3.24
-	github.com/swaggest/refl v1.0.1
+	github.com/swaggest/jsonschema-go v0.3.32
+	github.com/swaggest/refl v1.0.2
 	gopkg.in/yaml.v2 v2.4.0
 )
 
diff --git a/go.sum b/go.sum
index 94f9b5b..746c0ee 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
-github.com/bool64/dev v0.1.17/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.2.5 h1:H0bylghwcjDBBhEwSFTjArEO9Dr8cCaB54QSOF7esOA=
-github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
+github.com/bool64/dev v0.2.10 h1:ypAGBazcwyIy2JvIJio8V3kdqO7AgIAYvcckW54qxr4=
+github.com/bool64/dev v0.2.10/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
 github.com/bool64/shared v0.1.3 h1:gj7XZPYa1flQsCg3q9AIju+W2A1jaexK0fdFu2XtaG0=
 github.com/bool64/shared v0.1.3/go.mod h1:RF1p1Oi29ofgOvinBpetbF5mceOUP3kpMkvLbWOmtm0=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -54,15 +52,14 @@ github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/swaggest/assertjson v1.6.8 h1:1O/9UI5M+2OJI7BeEWKGj0wTvpRXZt5FkOJ4nRkY4rA=
 github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M=
-github.com/swaggest/jsonschema-go v0.3.24 h1:vVtFhWQWT9sgsvgMdh0n3O1F0+TnhOWz4+lbz5p5ChY=
-github.com/swaggest/jsonschema-go v0.3.24/go.mod h1:B2ZSqrrlkj21zhywlkh8VnKBmuqUwDv3dLQoPjgHk7M=
-github.com/swaggest/refl v1.0.0/go.mod h1:acYd5x8NNxivp+ZHdRZKJYz66n/qjo3Q9Sa/jAivljQ=
-github.com/swaggest/refl v1.0.1 h1:YQHb7Ic6EMpdUpxQmTWmf/O4IWN6iIErxJNWA7LwyyM=
-github.com/swaggest/refl v1.0.1/go.mod h1:dnx+n9YaI0o+FH+OR2tJZWLABBVIPs9qc4VY9UdrhLE=
+github.com/swaggest/jsonschema-go v0.3.32 h1:C8XiNEoR1L0QwBAWTvRyY0xYdEmEeJYthImF9GzSST0=
+github.com/swaggest/jsonschema-go v0.3.32/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
+github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88=
+github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g=
 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
diff --git a/openapi3/helper.go b/openapi3/helper.go
index 5f223f9..da2ed4a 100644
--- a/openapi3/helper.go
+++ b/openapi3/helper.go
@@ -126,3 +126,10 @@ func (s *Spec) AddOperation(method, path string, operation Operation) error {
 		return nil
 	})
 }
+
+// UnknownParamIsForbidden indicates forbidden unknown parameters.
+func (o Operation) UnknownParamIsForbidden(in ParameterIn) bool {
+	f, ok := o.MapOfAnything[xForbidUnknown+string(in)].(bool)
+
+	return f && ok
+}
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index 1d67dc4..a9cb54d 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -210,6 +210,12 @@ func (r *Reflector) parseRequestBody(
 	return nil
 }
 
+const (
+	// xForbidUnknown is a prefix of a vendor extension to indicate forbidden unknown parameters.
+	// It should be used together with ParameterIn as a suffix.
+	xForbidUnknown = "x-forbid-unknown-"
+)
+
 func (r *Reflector) parseParametersIn(
 	o *Operation, input interface{}, in ParameterIn, propertyMapping map[string]string,
 ) error {
@@ -217,11 +223,14 @@ func (r *Reflector) parseParametersIn(
 		return nil
 	}
 
-	_, err := r.Reflect(input,
+	s, err := r.Reflect(input,
 		jsonschema.DefinitionsPrefix("#/components/schemas/"),
 		jsonschema.CollectDefinitions(r.collectDefinition),
 		jsonschema.PropertyNameMapping(propertyMapping),
 		jsonschema.PropertyNameTag(string(in)),
+		func(rc *jsonschema.ReflectContext) {
+			rc.UnnamedFieldWithTag = true
+		},
 		jsonschema.SkipEmbeddedMapsSlices,
 		jsonschema.InterceptProperty(func(name string, field reflect.StructField, propertySchema *jsonschema.Schema) error {
 			s := SchemaOrRef{}
@@ -310,6 +319,12 @@ func (r *Reflector) parseParametersIn(
 		return err
 	}
 
+	if s.AdditionalProperties != nil &&
+		s.AdditionalProperties.TypeBoolean != nil &&
+		!*s.AdditionalProperties.TypeBoolean {
+		o.WithMapOfAnythingItem(xForbidUnknown+string(in), true)
+	}
+
 	return nil
 }
 


Test patch:
diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index a90a1ae..aeadbd1 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -6,20 +6,26 @@ on:
       - master
       - main
   pull_request:
+
+# Cancel the workflow in progress in newer build is about to start.
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
 env:
   GO111MODULE: "on"
   RUN_BASE_COVERAGE: "on"  # Runs test for PR base in case base test coverage is missing.
-  COV_GO_VERSION: 1.17.x   # Version of Go to collect coverage
+  COV_GO_VERSION: 1.18.x   # Version of Go to collect coverage
 jobs:
   test:
     strategy:
       matrix:
-        go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, tip ]
+        go-version: [ 1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, tip ]
     runs-on: ubuntu-latest
     steps:
       - name: Install Go stable
         if: matrix.go-version != 'tip'
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go-version }}
       - name: Install Go tip
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 3809573..cd2d19e 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -614,6 +614,52 @@ func TestReflector_SetupRequest_jsonQuery(t *testing.T) {
 	}`), r.SpecEns())
 }
 
+func TestReflector_SetupRequest_forbidParams(t *testing.T) {
+	type req struct {
+		Query  string `query:"query"`
+		Header string `header:"header"`
+		Cookie string `cookie:"cookie"`
+		Path   string `path:"path"`
+
+		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     new(req),
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/{path}", *oc.Operation))
+
+	assertjson.EqualMarshal(t, []byte(`{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/{path}":{
+		  "get":{
+			"parameters":[
+			  {"name":"query","in":"query","schema":{"type":"string"}},
+			  {
+				"name":"path","in":"path","required":true,
+				"schema":{"type":"string"}
+			  },
+			  {"name":"cookie","in":"cookie","schema":{"type":"string"}},
+			  {"name":"header","in":"header","schema":{"type":"string"}}
+			],
+			"responses":{"204":{"description":"No Content"}},
+			"x-forbid-unknown-cookie":true,"x-forbid-unknown-query":true
+		  }
+		}
+	  }
+	}`), r.SpecEns())
+
+	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInCookie))
+	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInHeader))
+	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInQuery))
+	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInPath))
+}
+
 func TestReflector_SetupRequest_noBody(t *testing.T) {
 	type req struct {
 		ID int `json:"id" path:"id"`