Skip to content

Commit 1e37fc1

Browse files
committed
address #191
1 parent 07b06eb commit 1e37fc1

6 files changed

Lines changed: 331 additions & 11 deletions

File tree

helpers/parameter_utilities.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
1+
// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
22
// SPDX-License-Identifier: MIT
33

44
package helpers
@@ -158,6 +158,19 @@ func ExtractSecurityHeaderNames(
158158
return headers
159159
}
160160

161+
// EffectiveSecurityForOperation returns the security requirements that apply to the
162+
// operation matched by request method. It implements OpenAPI's inheritance rule:
163+
// - If the operation defines security (even an empty array), use that.
164+
// - Otherwise, fall back to the document-level global security.
165+
// - Returns nil only when neither level defines security.
166+
func EffectiveSecurityForOperation(request *http.Request, item *v3.PathItem, docSecurity []*base.SecurityRequirement) []*base.SecurityRequirement {
167+
op := ExtractOperation(request, item)
168+
if op != nil && op.Security != nil {
169+
return op.Security // operation-level (may be empty [] = "no security")
170+
}
171+
return docSecurity // nil when no global security either
172+
}
173+
161174
func cast(v string) any {
162175
if v == "true" || v == "false" {
163176
b, _ := strconv.ParseBool(v)

helpers/parameter_utilities_test.go

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
1+
// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
22
// SPDX-License-Identifier: MIT
33

44
package helpers
@@ -938,7 +938,7 @@ func TestConstructParamMapFromPipeEncodingWithSchema(t *testing.T) {
938938
}
939939
result := ConstructParamMapFromPipeEncodingWithSchema(params, sch)
940940
props := result["key1"].(map[string]interface{})
941-
require.Equal(t, "123", props["name"]) // string because schema says string
941+
require.Equal(t, "123", props["name"]) // string because schema says string
942942
require.Equal(t, int64(42), props["count"]) // int because schema says integer
943943
}
944944

@@ -964,8 +964,8 @@ func TestConstructMapFromCSVWithSchema(t *testing.T) {
964964
}),
965965
}
966966
result := ConstructMapFromCSVWithSchema("id,99,rank,3.5", sch)
967-
require.Equal(t, "99", result["id"]) // string
968-
require.Equal(t, 3.5, result["rank"]) // number
967+
require.Equal(t, "99", result["id"]) // string
968+
require.Equal(t, 3.5, result["rank"]) // number
969969

970970
// odd number of values
971971
result = ConstructMapFromCSVWithSchema("id,99,rank", sch)
@@ -1042,3 +1042,65 @@ func TestConstructParamMapFromFormEncodingArrayWithSchema(t *testing.T) {
10421042
require.Equal(t, "val", props["key1"])
10431043
require.NotContains(t, props, "key2")
10441044
}
1045+
1046+
func TestEffectiveSecurityForOperation(t *testing.T) {
1047+
globalSecurity := []*base.SecurityRequirement{
1048+
{
1049+
Requirements: orderedmap.ToOrderedMap(map[string][]string{
1050+
"GlobalAuth": {},
1051+
}),
1052+
},
1053+
}
1054+
1055+
opSecurity := []*base.SecurityRequirement{
1056+
{
1057+
Requirements: orderedmap.ToOrderedMap(map[string][]string{
1058+
"OpAuth": {},
1059+
}),
1060+
},
1061+
}
1062+
1063+
t.Run("operation-level security wins over global", func(t *testing.T) {
1064+
pathItem := &v3.PathItem{
1065+
Get: &v3.Operation{Security: opSecurity},
1066+
}
1067+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
1068+
result := EffectiveSecurityForOperation(request, pathItem, globalSecurity)
1069+
require.Equal(t, opSecurity, result)
1070+
})
1071+
1072+
t.Run("nil operation security falls back to global", func(t *testing.T) {
1073+
pathItem := &v3.PathItem{
1074+
Get: &v3.Operation{}, // Security is nil
1075+
}
1076+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
1077+
result := EffectiveSecurityForOperation(request, pathItem, globalSecurity)
1078+
require.Equal(t, globalSecurity, result)
1079+
})
1080+
1081+
t.Run("empty operation security means no security (opt-out)", func(t *testing.T) {
1082+
pathItem := &v3.PathItem{
1083+
Get: &v3.Operation{Security: []*base.SecurityRequirement{}},
1084+
}
1085+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
1086+
result := EffectiveSecurityForOperation(request, pathItem, globalSecurity)
1087+
require.NotNil(t, result)
1088+
require.Len(t, result, 0)
1089+
})
1090+
1091+
t.Run("both nil returns nil", func(t *testing.T) {
1092+
pathItem := &v3.PathItem{
1093+
Get: &v3.Operation{},
1094+
}
1095+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
1096+
result := EffectiveSecurityForOperation(request, pathItem, nil)
1097+
require.Nil(t, result)
1098+
})
1099+
1100+
t.Run("nil operation falls back to global", func(t *testing.T) {
1101+
pathItem := &v3.PathItem{} // no Get operation
1102+
request, _ := http.NewRequest(http.MethodGet, "/", nil)
1103+
result := EffectiveSecurityForOperation(request, pathItem, globalSecurity)
1104+
require.Equal(t, globalSecurity, result)
1105+
})
1106+
}

parameters/header_parameters.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func (v *paramValidator) ValidateHeaderParamsWithPathItem(request *http.Request,
231231
// Extract security headers applicable to this operation
232232
var securityHeaders []string
233233
if v.document.Components != nil && v.document.Components.SecuritySchemes != nil {
234-
security := helpers.ExtractSecurityForOperation(request, pathItem)
234+
security := helpers.EffectiveSecurityForOperation(request, pathItem, v.document.Security)
235235
// Convert orderedmap to regular map for the helper
236236
schemesMap := make(map[string]*v3.SecurityScheme)
237237
for pair := v.document.Components.SecuritySchemes.First(); pair != nil; pair = pair.Next() {

parameters/header_parameters_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,3 +1417,72 @@ paths:
14171417
assert.Len(t, errors, 1)
14181418
assert.Contains(t, errors[0].Message, "X-Custom")
14191419
}
1420+
1421+
func TestNewValidator_HeaderParams_StrictMode_GlobalSecurityScheme(t *testing.T) {
1422+
// Global security with HTTP basic auth — Authorization header should not trigger undeclared error in strict mode
1423+
spec := `openapi: 3.1.0
1424+
info:
1425+
title: Test API
1426+
version: "1.0"
1427+
paths:
1428+
/secure/resource:
1429+
get:
1430+
responses:
1431+
"200":
1432+
description: OK
1433+
security:
1434+
- BasicAuth: []
1435+
components:
1436+
securitySchemes:
1437+
BasicAuth:
1438+
type: http
1439+
scheme: basic`
1440+
1441+
doc, _ := libopenapi.NewDocument([]byte(spec))
1442+
m, _ := doc.BuildV3Model()
1443+
v := NewParameterValidator(&m.Model, config.WithStrictMode())
1444+
1445+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil)
1446+
request.Header.Set("Authorization", "Basic dXNlcjpwYXNz")
1447+
1448+
valid, errors := v.ValidateHeaderParams(request)
1449+
1450+
// Authorization should be recognized as a valid header via global security
1451+
assert.True(t, valid)
1452+
assert.Len(t, errors, 0)
1453+
}
1454+
1455+
func TestNewValidator_HeaderParams_StrictMode_GlobalSecurityApiKey(t *testing.T) {
1456+
// Global security with apiKey in header — X-API-Key should not trigger undeclared error in strict mode
1457+
spec := `openapi: 3.1.0
1458+
info:
1459+
title: Test API
1460+
version: "1.0"
1461+
paths:
1462+
/secure/resource:
1463+
get:
1464+
responses:
1465+
"200":
1466+
description: OK
1467+
security:
1468+
- ApiKeyAuth: []
1469+
components:
1470+
securitySchemes:
1471+
ApiKeyAuth:
1472+
type: apiKey
1473+
in: header
1474+
name: X-API-Key`
1475+
1476+
doc, _ := libopenapi.NewDocument([]byte(spec))
1477+
m, _ := doc.BuildV3Model()
1478+
v := NewParameterValidator(&m.Model, config.WithStrictMode())
1479+
1480+
request, _ := http.NewRequest(http.MethodGet, "https://things.com/secure/resource", nil)
1481+
request.Header.Set("X-API-Key", "my-secret-key")
1482+
1483+
valid, errors := v.ValidateHeaderParams(request)
1484+
1485+
// X-API-Key should be recognized as a valid header via global security
1486+
assert.True(t, valid)
1487+
assert.Len(t, errors, 0)
1488+
}

parameters/validate_security.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
1+
// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley
22
// SPDX-License-Identifier: MIT
33

44
package parameters
@@ -42,10 +42,10 @@ func (v *paramValidator) ValidateSecurityWithPathItem(request *http.Request, pat
4242
if !v.options.SecurityValidation {
4343
return true, nil
4444
}
45-
// extract security for the operation
46-
security := helpers.ExtractSecurityForOperation(request, pathItem)
45+
// extract security for the operation, falling back to document-level global security
46+
security := helpers.EffectiveSecurityForOperation(request, pathItem, v.document.Security)
4747

48-
if security == nil {
48+
if len(security) == 0 {
4949
return true, nil
5050
}
5151

0 commit comments

Comments
 (0)