Skip to content

Commit

Permalink
fix: added fix to validate for top-level type in parameter
Browse files Browse the repository at this point in the history
schemas while using oneOf in request-validator plugin.

As of now, deck file openapi2kong command was not
checking for multiple types used while creating parameter
schemas with oneOf, which is not supported by Kong request-
validator plugin. Thus, we are forcing for defining
a top-level type property and erroring out in case
it is not present.
  • Loading branch information
Prashansa-K committed Oct 23, 2024
1 parent e0a80fc commit 45ac9b7
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 15 deletions.
6 changes: 3 additions & 3 deletions openapi2kong/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ func dereferenceSchema(sr *base.SchemaProxy, seenBefore map[string]*base.SchemaP
// extractSchema will extract a schema, including all sub-schemas/references and
// return it as a single JSONschema string. All components will be moved under the
// "#/definitions/" key.
func extractSchema(s *base.SchemaProxy) string {
func extractSchema(s *base.SchemaProxy) (string, map[string]interface{}) {
if s == nil || s.Schema() == nil {
return ""
return "", nil
}

seenBefore := make(map[string]*base.SchemaProxy)
Expand Down Expand Up @@ -92,5 +92,5 @@ func extractSchema(s *base.SchemaProxy) string {

result, _ := json.Marshal(finalSchema)
// update the $ref values; this is safe because plain " (double-quotes) would be escaped if in actual values
return strings.ReplaceAll(string(result), "\"$ref\":\"#/components/schemas/", "\"$ref\":\"#/definitions/")
return strings.ReplaceAll(string(result), "\"$ref\":\"#/components/schemas/", "\"$ref\":\"#/definitions/"), finalSchema
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"_format_version": "3.0",
"services": [
{
"host": "backend.com",
"id": "730d612d-914b-5fe8-8ead-e6aa654318ef",
"name": "example",
"path": "/path",
"plugins": [],
"port": 80,
"protocol": "http",
"routes": [
{
"id": "1446ecde-7037-5f9c-8537-8217e2a12bfa",
"methods": [
"GET"
],
"name": "example_params-test_get",
"paths": [
"~/params/test$"
],
"plugins": [
{
"config": {
"body_schema": "{}",
"parameter_schema": [
{
"explode": true,
"in": "query",
"name": "queryid",
"required": true,
"schema": "{\"oneOf\":[{\"example\":10,\"type\":\"integer\"},{\"example\":2.5,\"type\":\"number\"}],\"type\":\"number\"}",
"style": "form"
},
{
"explode": false,
"in": "header",
"name": "testHeader",
"required": true,
"schema": "{\"$ref\":\"#/definitions/headerType\",\"definitions\":{\"headerType\":{\"oneOf\":[{\"example\":\"10\",\"type\":\"string\"},{\"example\":2.5,\"type\":\"number\"}],\"type\":\"string\"}}}",
"style": "simple"
},
{
"explode": false,
"in": "header",
"name": "testHeader",
"required": true,
"schema": "{\"$ref\":\"#/definitions/secondHeaderType\",\"definitions\":{\"secondHeaderType\":{\"oneOf\":[{\"example\":\"10\",\"type\":\"string\"},{\"example\":2.5,\"type\":\"number\"}],\"type\":\"string\"}}}",
"style": "simple"
}
],
"version": "draft4"
},
"enabled": true,
"id": "8bd60198-9b34-5f0b-9240-4826c7c331a0",
"name": "request-validator",
"tags": [
"OAS3_import",
"OAS3file_17-request-validator-plugin-oneOf-usage.yaml"
]
}
],
"regex_priority": 200,
"strip_path": false,
"tags": [
"OAS3_import",
"OAS3file_17-request-validator-plugin-oneOf-usage.yaml"
]
}
],
"tags": [
"OAS3_import",
"OAS3file_17-request-validator-plugin-oneOf-usage.yaml"
]
}
],
"upstreams": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# When the request-validator is added without a body or parameter schema
# the generator should automatically generate it.

openapi: 3.0.2

info:
title: Example
version: 1.0.0

servers:
- url: http://backend.com/path

x-kong-plugin-request-validator: {}

paths:
/params/test:
get:
x-kong-plugin-request-validator:
enabled: true
config:
body_schema: '{}'
parameters:
- in: query
name: queryid
schema:
type: number
oneOf:
- type: integer
example: 10
- type: number
example: 2.5
required: true
- in: header
name: testHeader
schema:
$ref: '#/components/schemas/headerType'
required: true
- in: header
name: testHeader
schema:
$ref: '#/components/schemas/secondHeaderType'
required: true
components:
schemas:
headerType:
type: string
oneOf:
- type: string
example: "10"
- type: number
example: 2.5
secondHeaderType:
$ref: '#/components/schemas/headerType'
6 changes: 5 additions & 1 deletion openapi2kong/openapi2kong.go
Original file line number Diff line number Diff line change
Expand Up @@ -1027,8 +1027,12 @@ func Convert(content []byte, opts O2kOptions) (map[string]interface{}, error) {

// Extract the request-validator config from the plugin list, generate it and reinsert
operationValidatorConfig, operationPluginList = getValidatorPlugin(operationPluginList, pathValidatorConfig)
validatorPlugin := generateValidatorPlugin(operationValidatorConfig, operation, opts.UUIDNamespace,
validatorPlugin, err := generateValidatorPlugin(operationValidatorConfig, operation, opts.UUIDNamespace,
operationBaseName, opts.SkipID, opts.InsoCompat)
if err != nil {
return nil, fmt.Errorf("failed to create validator plugin: %w", err)
}

operationPluginList = insertPlugin(operationPluginList, validatorPlugin)

// construct the route
Expand Down
84 changes: 73 additions & 11 deletions openapi2kong/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package openapi2kong

import (
"encoding/json"
"fmt"
"mime"
"sort"
"strings"
Expand Down Expand Up @@ -33,14 +34,14 @@ func getDefaultParamStyle(givenStyle string, paramType string) string {
// generateParameterSchema returns the given schema if there is one, a generated
// schema if it was specified, or nil if there is none.
// Parameters include path, query, and headers
func generateParameterSchema(operation *v3.Operation, insoCompat bool) []map[string]interface{} {
func generateParameterSchema(operation *v3.Operation, insoCompat bool) ([]map[string]interface{}, error) {
parameters := operation.Parameters
if parameters == nil {
return nil
return nil, nil
}

if len(parameters) == 0 {
return nil
return nil, nil
}

result := make([]map[string]interface{}, len(parameters))
Expand Down Expand Up @@ -72,17 +73,22 @@ func generateParameterSchema(operation *v3.Operation, insoCompat bool) []map[str
paramConf["required"] = false
}

schema := extractSchema(parameter.Schema)
schema, schemaMap := extractSchema(parameter.Schema)
if schema != "" {
paramConf["schema"] = schema

_, typeStr, ok := fetchOneOfAndType(schemaMap)
if ok && typeStr == "" {
return nil, fmt.Errorf(`parameter schemas for request-validator plugin using oneOf must have a top-level type property`)
}
}

result[i] = paramConf
i++
}
}

return result
return result, nil
}

func parseMediaType(mediaType string) (string, string, error) {
Expand Down Expand Up @@ -119,7 +125,8 @@ func generateBodySchema(operation *v3.Operation) string {
return ""
}
if typ == "application" && (subtype == "json" || strings.HasSuffix(subtype, "+json")) {
return extractSchema((*contentValue).Schema)
schema, _ := extractSchema((*contentValue).Schema)
return schema
}

contentItem = contentItem.Next()
Expand Down Expand Up @@ -162,9 +169,9 @@ func generateContentTypes(operation *v3.Operation) []string {
// on the JSON snippet, and the OAS inputs. This can return nil
func generateValidatorPlugin(configJSON []byte, operation *v3.Operation,
uuidNamespace uuid.UUID, baseName string, skipID bool, insoCompat bool,
) *map[string]interface{} {
) (*map[string]interface{}, error) {
if len(configJSON) == 0 {
return nil
return nil, nil
}
logbasics.Debug("generating validator plugin", "operation", baseName)

Expand All @@ -183,7 +190,10 @@ func generateValidatorPlugin(configJSON []byte, operation *v3.Operation,
}

if config["parameter_schema"] == nil {
parameterSchema := generateParameterSchema(operation, insoCompat)
parameterSchema, err := generateParameterSchema(operation, insoCompat)
if err != nil {
return nil, err
}
if parameterSchema != nil {
config["parameter_schema"] = parameterSchema
config["version"] = JSONSchemaVersion
Expand All @@ -201,7 +211,7 @@ func generateValidatorPlugin(configJSON []byte, operation *v3.Operation,
// unless the content-types have been provided by the user
if config["allowed_content_types"] == nil {
// also not provided, so really nothing to validate, don't add a plugin
return nil
return nil, nil
}
// add an empty schema, which passes everything, but it also activates the
// content-type check
Expand All @@ -218,5 +228,57 @@ func generateValidatorPlugin(configJSON []byte, operation *v3.Operation,
}
}

return &pluginConfig
return &pluginConfig, nil
}

func fetchOneOfAndType(schemaMap map[string]interface{}) ([]interface{}, string, bool) {
var oneOfSchemaArray []interface{}
var typeStr string
var oneOfFound bool

// Check if oneOf exists at the current level
if oneOf, ok := schemaMap["oneOf"]; ok {
if slice, isSlice := oneOf.([]interface{}); isSlice {
oneOfSchemaArray = slice
oneOfFound = true
}
}

// Check if type exists at the current level
if typ, ok := schemaMap["type"]; ok {
if str, isString := typ.(string); isString {
typeStr = str
}
}

// If both oneOf and type are found at this level, return them
if oneOfFound && typeStr != "" {
return oneOfSchemaArray, typeStr, true
}

// Recursively search in nested objects
for _, value := range schemaMap {
switch v := value.(type) {
case map[string]interface{}:
if slice, str, found := fetchOneOfAndType(v); found {
return slice, str, true
}
case []interface{}:
for _, item := range v {
if itemMap, isMap := item.(map[string]interface{}); isMap {
if slice, str, found := fetchOneOfAndType(itemMap); found {
return slice, str, true
}
}
}
}
}

// If oneOf is found but type is not, return oneOf with empty type
if oneOfFound {
return oneOfSchemaArray, "", true
}

// If neither oneOf nor type is found
return nil, "", false
}

0 comments on commit 45ac9b7

Please sign in to comment.