-
Notifications
You must be signed in to change notification settings - Fork 44
Expand file tree
/
Copy pathvalidator.go
More file actions
572 lines (485 loc) · 19.8 KB
/
validator.go
File metadata and controls
572 lines (485 loc) · 19.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
// SPDX-License-Identifier: MIT
package validator
import (
"fmt"
"net/http"
"sort"
"sync"
"github.com/pb33f/libopenapi"
"github.com/pb33f/libopenapi/datamodel/high/base"
"github.com/pb33f/libopenapi/utils"
"go.yaml.in/yaml/v4"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi-validator/cache"
"github.com/pb33f/libopenapi-validator/config"
"github.com/pb33f/libopenapi-validator/errors"
"github.com/pb33f/libopenapi-validator/helpers"
"github.com/pb33f/libopenapi-validator/parameters"
"github.com/pb33f/libopenapi-validator/paths"
"github.com/pb33f/libopenapi-validator/radix"
"github.com/pb33f/libopenapi-validator/requests"
"github.com/pb33f/libopenapi-validator/responses"
"github.com/pb33f/libopenapi-validator/schema_validation"
)
// Validator provides a coarse grained interface for validating an OpenAPI 3+ documents.
// There are three primary use-cases for validation
//
// Validating *http.Request objects against and OpenAPI 3+ document
// Validating *http.Response objects against an OpenAPI 3+ document
// Validating an OpenAPI 3+ document against the OpenAPI 3+ specification
type Validator interface {
// ValidateHttpRequest will validate an *http.Request object against an OpenAPI 3+ document.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError)
// ValidateHttpRequestSync will validate an *http.Request object against an OpenAPI 3+ document synchronously and without spawning any goroutines.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError)
// ValidateHttpRequestWithPathItem will validate an *http.Request object against an OpenAPI 3+ document.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError)
// ValidateHttpRequestSyncWithPathItem will validate an *http.Request object against an OpenAPI 3+ document synchronously and without spawning any goroutines.
// The path, query, cookie and header parameters and request body are validated.
ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError)
// ValidateHttpResponse will an *http.Response object against an OpenAPI 3+ document.
// The response body is validated. The request is only used to extract the correct response from the spec.
ValidateHttpResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError)
// ValidateHttpRequestResponse will validate both the *http.Request and *http.Response objects against an OpenAPI 3+ document.
// The path, query, cookie and header parameters and request and response body are validated.
ValidateHttpRequestResponse(request *http.Request, response *http.Response) (bool, []*errors.ValidationError)
// ValidateDocument will validate an OpenAPI 3+ document against the 3.0 or 3.1 OpenAPI 3+ specification
ValidateDocument() (bool, []*errors.ValidationError)
// GetParameterValidator will return a parameters.ParameterValidator instance used to validate parameters
GetParameterValidator() parameters.ParameterValidator
// GetRequestBodyValidator will return a parameters.RequestBodyValidator instance used to validate request bodies
GetRequestBodyValidator() requests.RequestBodyValidator
// GetResponseBodyValidator will return a parameters.ResponseBodyValidator instance used to validate response bodies
GetResponseBodyValidator() responses.ResponseBodyValidator
// SetDocument will set the OpenAPI 3+ document to be validated
SetDocument(document libopenapi.Document)
}
// NewValidator will create a new Validator from an OpenAPI 3+ document
func NewValidator(document libopenapi.Document, opts ...config.Option) (Validator, []error) {
m, errs := document.BuildV3Model()
if errs != nil {
return nil, []error{errs}
}
v := NewValidatorFromV3Model(&m.Model, opts...)
v.(*validator).document = document
return v, nil
}
// NewValidatorFromV3Model will create a new Validator from an OpenAPI Model
func NewValidatorFromV3Model(m *v3.Document, opts ...config.Option) Validator {
options := config.NewValidationOptions(opts...)
// Build radix tree for O(k) path lookup (where k = path depth)
// Skip if path tree is disabled or a custom tree was provided
if options.PathTree == nil && !options.IsPathTreeDisabled() {
options.PathTree = radix.BuildPathTree(m)
}
// warm the schema caches by pre-compiling all schemas in the document
// (warmSchemaCaches checks for nil cache and skips if disabled)
warmSchemaCaches(m, options)
v := &validator{options: options, v3Model: m}
// create a new parameter validator
v.paramValidator = parameters.NewParameterValidator(m, config.WithExistingOpts(options))
// create aq new request body validator
v.requestValidator = requests.NewRequestBodyValidator(m, config.WithExistingOpts(options))
// create a response body validator
v.responseValidator = responses.NewResponseBodyValidator(m, config.WithExistingOpts(options))
return v
}
func (v *validator) SetDocument(document libopenapi.Document) {
v.document = document
}
func (v *validator) GetParameterValidator() parameters.ParameterValidator {
return v.paramValidator
}
func (v *validator) GetRequestBodyValidator() requests.RequestBodyValidator {
return v.requestValidator
}
func (v *validator) GetResponseBodyValidator() responses.ResponseBodyValidator {
return v.responseValidator
}
func (v *validator) ValidateDocument() (bool, []*errors.ValidationError) {
if v.document == nil {
return false, []*errors.ValidationError{{
ValidationType: helpers.DocumentValidation,
ValidationSubType: helpers.ValidationMissing,
Message: "Document is not set",
Reason: "The document cannot be validated as it is not set",
SpecLine: 1,
SpecCol: 1,
HowToFix: "Set the document via `SetDocument` before validating",
}}
}
var validationOpts []config.Option
if v.options != nil {
validationOpts = append(validationOpts, config.WithRegexEngine(v.options.RegexEngine))
}
return schema_validation.ValidateOpenAPIDocument(v.document, validationOpts...)
}
func (v *validator) ValidateHttpResponse(
request *http.Request,
response *http.Response,
) (bool, []*errors.ValidationError) {
var pathItem *v3.PathItem
var pathValue string
var errs []*errors.ValidationError
pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options)
if pathItem == nil || errs != nil {
return false, errs
}
responseBodyValidator := v.responseValidator
// validate response
_, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue)
if len(responseErrors) > 0 {
return false, responseErrors
}
return true, nil
}
func (v *validator) ValidateHttpRequestResponse(
request *http.Request,
response *http.Response,
) (bool, []*errors.ValidationError) {
var pathItem *v3.PathItem
var pathValue string
var errs []*errors.ValidationError
pathItem, errs, pathValue = paths.FindPath(request, v.v3Model, v.options)
if pathItem == nil || errs != nil {
return false, errs
}
responseBodyValidator := v.responseValidator
// validate request and response
_, requestErrors := v.ValidateHttpRequestWithPathItem(request, pathItem, pathValue)
_, responseErrors := responseBodyValidator.ValidateResponseBodyWithPathItem(request, response, pathItem, pathValue)
if len(requestErrors) > 0 || len(responseErrors) > 0 {
return false, append(requestErrors, responseErrors...)
}
return true, nil
}
func (v *validator) ValidateHttpRequest(request *http.Request) (bool, []*errors.ValidationError) {
pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options)
if len(errs) > 0 {
return false, errs
}
return v.ValidateHttpRequestWithPathItem(request, pathItem, foundPath)
}
func (v *validator) ValidateHttpRequestWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) {
// create a new parameter validator
paramValidator := v.paramValidator
// create a new request body validator
reqBodyValidator := v.requestValidator
// create some channels to handle async validation
doneChan := make(chan struct{})
errChan := make(chan []*errors.ValidationError)
controlChan := make(chan struct{})
// async param validation function.
parameterValidationFunc := func(control chan struct{}, errorChan chan []*errors.ValidationError) {
paramErrs := make(chan []*errors.ValidationError)
paramControlChan := make(chan struct{})
paramFunctionControlChan := make(chan struct{})
var paramValidationErrors []*errors.ValidationError
validations := []validationFunction{
paramValidator.ValidatePathParamsWithPathItem,
paramValidator.ValidateCookieParamsWithPathItem,
paramValidator.ValidateHeaderParamsWithPathItem,
paramValidator.ValidateQueryParamsWithPathItem,
paramValidator.ValidateSecurityWithPathItem,
}
// listen for validation errors on parameters. everything will run async.
paramListener := func(control chan struct{}, errorChan chan []*errors.ValidationError) {
completedValidations := 0
for {
select {
case vErrs := <-errorChan:
paramValidationErrors = append(paramValidationErrors, vErrs...)
case <-control:
completedValidations++
if completedValidations == len(validations) {
paramFunctionControlChan <- struct{}{}
return
}
}
}
}
validateParamFunction := func(
control chan struct{},
errorChan chan []*errors.ValidationError,
validatorFunc validationFunction,
) {
valid, pErrs := validatorFunc(request, pathItem, pathValue)
if !valid {
errorChan <- pErrs
}
control <- struct{}{}
}
go paramListener(paramControlChan, paramErrs)
for i := range validations {
go validateParamFunction(paramControlChan, paramErrs, validations[i])
}
// wait for all the validations to complete
<-paramFunctionControlChan
if len(paramValidationErrors) > 0 {
errorChan <- paramValidationErrors
}
// let runValidation know we are done with this part.
controlChan <- struct{}{}
}
requestBodyValidationFunc := func(control chan struct{}, errorChan chan []*errors.ValidationError) {
valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue)
if !valid {
errorChan <- pErrs
}
control <- struct{}{}
}
// build async functions
asyncFunctions := []validationFunctionAsync{
parameterValidationFunc,
requestBodyValidationFunc,
}
var validationErrors []*errors.ValidationError
// sit and wait for everything to report back.
go runValidation(controlChan, doneChan, errChan, &validationErrors, len(asyncFunctions))
// run async functions
for i := range asyncFunctions {
go asyncFunctions[i](controlChan, errChan)
}
// wait for all the validations to complete
<-doneChan
// sort errors for deterministic ordering (async validation can return errors in any order)
sortValidationErrors(validationErrors)
return len(validationErrors) == 0, validationErrors
}
func (v *validator) ValidateHttpRequestSync(request *http.Request) (bool, []*errors.ValidationError) {
pathItem, errs, foundPath := paths.FindPath(request, v.v3Model, v.options)
if len(errs) > 0 {
return false, errs
}
return v.ValidateHttpRequestSyncWithPathItem(request, pathItem, foundPath)
}
func (v *validator) ValidateHttpRequestSyncWithPathItem(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError) {
// create a new parameter validator
paramValidator := v.paramValidator
// create a new request body validator
reqBodyValidator := v.requestValidator
validationErrors := make([]*errors.ValidationError, 0)
paramValidationErrors := make([]*errors.ValidationError, 0)
for _, validateFunc := range []validationFunction{
paramValidator.ValidatePathParamsWithPathItem,
paramValidator.ValidateCookieParamsWithPathItem,
paramValidator.ValidateHeaderParamsWithPathItem,
paramValidator.ValidateQueryParamsWithPathItem,
paramValidator.ValidateSecurityWithPathItem,
} {
valid, pErrs := validateFunc(request, pathItem, pathValue)
if !valid {
paramValidationErrors = append(paramValidationErrors, pErrs...)
}
}
valid, pErrs := reqBodyValidator.ValidateRequestBodyWithPathItem(request, pathItem, pathValue)
if !valid {
paramValidationErrors = append(paramValidationErrors, pErrs...)
}
validationErrors = append(validationErrors, paramValidationErrors...)
return len(validationErrors) == 0, validationErrors
}
type validator struct {
options *config.ValidationOptions
v3Model *v3.Document
document libopenapi.Document
paramValidator parameters.ParameterValidator
requestValidator requests.RequestBodyValidator
responseValidator responses.ResponseBodyValidator
}
func runValidation(control, doneChan chan struct{},
errorChan chan []*errors.ValidationError,
validationErrors *[]*errors.ValidationError,
total int,
) {
var validationLock sync.Mutex
completedValidations := 0
for {
select {
case vErrs := <-errorChan:
validationLock.Lock()
*validationErrors = append(*validationErrors, vErrs...)
validationLock.Unlock()
case <-control:
completedValidations++
if completedValidations == total {
doneChan <- struct{}{}
return
}
}
}
}
type (
validationFunction func(request *http.Request, pathItem *v3.PathItem, pathValue string) (bool, []*errors.ValidationError)
validationFunctionAsync func(control chan struct{}, errorChan chan []*errors.ValidationError)
)
// sortValidationErrors sorts validation errors for deterministic ordering.
// Errors are sorted by validation type first, then by message.
func sortValidationErrors(errs []*errors.ValidationError) {
sort.Slice(errs, func(i, j int) bool {
if errs[i].ValidationType != errs[j].ValidationType {
return errs[i].ValidationType < errs[j].ValidationType
}
return errs[i].Message < errs[j].Message
})
}
// warmSchemaCaches pre-compiles all schemas in the OpenAPI document and stores them in the validator caches.
// This frontloads the compilation cost so that runtime validation doesn't need to compile schemas.
func warmSchemaCaches(
doc *v3.Document,
options *config.ValidationOptions,
) {
// Skip warming if cache is nil (explicitly disabled via WithSchemaCache(nil))
if doc == nil || doc.Paths == nil || doc.Paths.PathItems == nil || options.SchemaCache == nil {
return
}
schemaCache := options.SchemaCache
// Walk through all paths and operations
for pathPair := doc.Paths.PathItems.First(); pathPair != nil; pathPair = pathPair.Next() {
pathItem := pathPair.Value()
// Get all operations for this path (handles all HTTP methods including OpenAPI 3.2+ extensions)
operations := pathItem.GetOperations()
if operations == nil {
continue
}
for opPair := operations.First(); opPair != nil; opPair = opPair.Next() {
operation := opPair.Value()
if operation == nil {
continue
}
// Warm request body schemas
if operation.RequestBody != nil && operation.RequestBody.Content != nil {
for contentPair := operation.RequestBody.Content.First(); contentPair != nil; contentPair = contentPair.Next() {
mediaType := contentPair.Value()
if mediaType.Schema != nil {
warmMediaTypeSchema(mediaType, schemaCache, options)
}
}
}
// Warm response body schemas
if operation.Responses != nil {
// Warm status code responses
if operation.Responses.Codes != nil {
for codePair := operation.Responses.Codes.First(); codePair != nil; codePair = codePair.Next() {
response := codePair.Value()
if response != nil && response.Content != nil {
for contentPair := response.Content.First(); contentPair != nil; contentPair = contentPair.Next() {
mediaType := contentPair.Value()
if mediaType.Schema != nil {
warmMediaTypeSchema(mediaType, schemaCache, options)
}
}
}
}
}
// Warm default response schemas
if operation.Responses.Default != nil && operation.Responses.Default.Content != nil {
for contentPair := operation.Responses.Default.Content.First(); contentPair != nil; contentPair = contentPair.Next() {
mediaType := contentPair.Value()
if mediaType.Schema != nil {
warmMediaTypeSchema(mediaType, schemaCache, options)
}
}
}
}
// Warm parameter schemas
if operation.Parameters != nil {
for _, param := range operation.Parameters {
if param != nil {
warmParameterSchema(param, schemaCache, options)
}
}
}
}
// Warm path-level parameters
if pathItem.Parameters != nil {
for _, param := range pathItem.Parameters {
if param != nil {
warmParameterSchema(param, schemaCache, options)
}
}
}
}
}
// warmMediaTypeSchema warms the cache for a media type schema
func warmMediaTypeSchema(mediaType *v3.MediaType, schemaCache cache.SchemaCache, options *config.ValidationOptions) {
if mediaType != nil && mediaType.Schema != nil {
hash := mediaType.GoLow().Schema.Value.Hash()
if _, exists := schemaCache.Load(hash); !exists {
schema := mediaType.Schema.Schema()
if schema != nil {
renderCtx := base.NewInlineRenderContextForValidation()
renderedInline, _ := schema.RenderInlineWithContext(renderCtx)
referenceSchema := string(renderedInline)
renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline)
if len(renderedInline) > 0 {
compiledSchema, _ := helpers.NewCompiledSchema(fmt.Sprintf("%x", hash), renderedJSON, options)
// Pre-parse YAML node for error reporting (avoids re-parsing on each error)
var renderedNode yaml.Node
_ = yaml.Unmarshal(renderedInline, &renderedNode)
schemaCache.Store(hash, &cache.SchemaCacheEntry{
Schema: schema,
RenderedInline: renderedInline,
ReferenceSchema: referenceSchema,
RenderedJSON: renderedJSON,
CompiledSchema: compiledSchema,
RenderedNode: &renderedNode,
})
}
}
}
}
}
// warmParameterSchema warms the cache for a parameter schema
func warmParameterSchema(param *v3.Parameter, schemaCache cache.SchemaCache, options *config.ValidationOptions) {
if param != nil {
var schema *base.Schema
var hash uint64
// Parameters can have schemas in two places: schema property or content property
if param.Schema != nil {
schema = param.Schema.Schema()
if schema != nil {
hash = param.GoLow().Schema.Value.Hash()
}
} else if param.Content != nil {
// Check content for schema
for contentPair := param.Content.First(); contentPair != nil; contentPair = contentPair.Next() {
mediaType := contentPair.Value()
if mediaType.Schema != nil {
schema = mediaType.Schema.Schema()
if schema != nil {
hash = mediaType.GoLow().Schema.Value.Hash()
}
break // Only process first content type
}
}
}
if schema != nil {
if _, exists := schemaCache.Load(hash); !exists {
renderCtx := base.NewInlineRenderContextForValidation()
renderedInline, _ := schema.RenderInlineWithContext(renderCtx)
referenceSchema := string(renderedInline)
renderedJSON, _ := utils.ConvertYAMLtoJSON(renderedInline)
if len(renderedInline) > 0 {
compiledSchema, _ := helpers.NewCompiledSchema(fmt.Sprintf("%x", hash), renderedJSON, options)
// Pre-parse YAML node for error reporting (avoids re-parsing on each error)
var renderedNode yaml.Node
_ = yaml.Unmarshal(renderedInline, &renderedNode)
// Store in cache using the shared SchemaCache type
schemaCache.Store(hash, &cache.SchemaCacheEntry{
Schema: schema,
RenderedInline: renderedInline,
ReferenceSchema: referenceSchema,
RenderedJSON: renderedJSON,
CompiledSchema: compiledSchema,
RenderedNode: &renderedNode,
})
}
}
}
}
}