Request validator plugin respects existing properties and generates required ones (#3283)

This commit is contained in:
Opender Singh 2021-04-20 13:04:22 +12:00 committed by GitHub
parent 40deacb718
commit 874f6c9b27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 535 additions and 26 deletions

View File

@ -0,0 +1,93 @@
{
"_format_version": "1.1",
"services": [
{
"name": "Example",
"plugins": [],
"routes": [
{
"methods": [
"POST"
],
"name": "Example-body-post",
"paths": [
"/body$"
],
"plugins": [
{
"config": {
"allowed_content_types": [
"application/json",
"application/xml"
],
"body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}",
"verbose_response": true,
"version": "draft4"
},
"enabled": true,
"name": "request-validator"
}
],
"strip_path": false,
"tags": [
"OAS3_import",
"OAS3file_request-validator-plugin.yaml"
]
},
{
"methods": [
"GET"
],
"name": "Example-params-get",
"paths": [
"/params$"
],
"plugins": [
{
"config": {
"parameter_schema": [
{
"explode": false,
"in": "path",
"name": "userId",
"required": true,
"schema": "{\"type\":\"integer\"}",
"style": "form"
}
],
"verbose_response": true,
"version": "draft4"
},
"enabled": true,
"name": "request-validator"
}
],
"strip_path": false,
"tags": [
"OAS3_import",
"OAS3file_request-validator-plugin.yaml"
]
}
],
"tags": [
"OAS3_import",
"OAS3file_request-validator-plugin.yaml"
],
"url": "http://backend.com/path"
}
],
"upstreams": [
{
"name": "Example",
"tags": [
"OAS3_import",
"OAS3file_request-validator-plugin.yaml"
],
"targets": [
{
"target": "backend.com:80"
}
]
}
]
}

View File

@ -0,0 +1,51 @@
openapi: 3.0.2
info:
title: Example
version: 1.0.0
servers:
- url: http://backend.com/path
paths:
/params:
get:
x-kong-plugin-request-validator:
enabled: true
config:
verbose_response: true
parameters:
- in: path
name: userId
schema:
type: integer
required: true
/body:
post:
x-kong-plugin-request-validator:
enabled: true
config:
verbose_response: true
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/jsonSchema'
application/xml:
schema:
$ref: '#/components/schemas/xmlSchema'
components:
schemas:
jsonSchema:
type: object
properties:
id:
type: integer
name:
type: string
xmlSchema:
type: object
properties:
prop:
type: integer

View File

@ -1,6 +1,6 @@
// @flow
import { generateServerPlugins, generatePlugin } from '../plugins';
import { generateServerPlugins, generatePlugin, generateRequestValidatorPlugin } from '../plugins';
describe('plugins', () => {
describe('generateServerPlugins()', () => {
@ -63,4 +63,306 @@ describe('plugins', () => {
});
});
});
describe('generateRequestValidatorPlugin()', () => {
const parameterSchema = [
{
schema: '{"anyOf":[{"type":"string"}]}',
style: 'form',
in: 'path',
name: 'human_timestamp',
required: true,
explode: false,
},
];
it('should retain config properties', () => {
const plugin = {
enabled: true,
config: {
parameter_schema: [parameterSchema],
body_schema: '[{"name":{"type": "string", "required": true}}]',
verbose_response: true,
allowed_content_types: ['application/json'],
},
};
const operation = {};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated).toStrictEqual({
name: 'request-validator',
enabled: plugin.enabled,
config: { version: 'draft4', ...plugin.config },
});
});
it('should not add properties if they are not defined', () => {
const plugin = {
enabled: true,
config: {
parameter_schema: [parameterSchema],
body_schema: '[{"name":{"type": "string", "required": true}}]',
// The following properties are missing
// verbose_response: true,
// allowed_content_types: ['application/json'],
},
};
const operation = {};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated).toStrictEqual({
name: 'request-validator',
enabled: plugin.enabled,
config: { version: 'draft4', ...plugin.config },
});
});
describe('parameter_schema', () => {
it('should not add parameter_schema if no parameters present', () => {
const plugin = {
enabled: true,
config: {},
};
const generated1 = generateRequestValidatorPlugin(plugin, {});
const generated2 = generateRequestValidatorPlugin(plugin, { parameters: [] });
expect(generated1.config).toStrictEqual({
version: 'draft4',
body_schema: '{}',
});
expect(generated2.config).toStrictEqual({
version: 'draft4',
body_schema: '{}',
});
});
it('should convert operation parameters to parameter_schema', () => {
const plugin = {
config: {},
};
const param = {
in: 'query',
explode: true,
required: false,
name: 'some_name',
schema: {
anyOf: [{ type: 'string' }],
},
style: 'form',
};
const operation: OA3Operation = {
parameters: [param],
};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
{
schema: '{"anyOf":[{"type":"string"}]}',
style: param.style,
in: param.in,
name: param.name,
explode: param.explode,
required: param.required,
},
],
});
});
it('should return default if operation parameter schema not defined on any parameters', () => {
const plugin = {};
const operation: OA3Operation = {
parameters: [
{
in: 'query',
name: 'some_name',
},
],
};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
{
explode: false,
in: 'query',
name: 'some_name',
required: false,
schema: '{}',
style: 'form',
},
],
});
});
it('should ignore parameters without schema', () => {
const plugin = {};
const paramWithSchema = {
in: 'query',
explode: true,
required: false,
name: 'some_name',
schema: {
anyOf: [{ type: 'string' }],
},
style: 'form',
};
const paramWithoutSchema = {
in: 'query',
name: 'some_name',
};
const operation: OA3Operation = {
parameters: [paramWithSchema, paramWithoutSchema],
};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
{
schema: '{"anyOf":[{"type":"string"}]}',
style: paramWithSchema.style,
in: paramWithSchema.in,
name: paramWithSchema.name,
explode: paramWithSchema.explode,
required: paramWithSchema.required,
},
{
schema: '{}',
style: 'form',
in: paramWithoutSchema.in,
name: paramWithoutSchema.name,
explode: false,
required: false,
},
],
});
});
});
describe('body_schema and allowed_content_types', () => {
it('should not add body_schema or allowed_content_types if no body present', () => {
const plugin = {
config: {},
};
const operation = {};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: '{}',
});
});
it('should return default if no operation request body content defined', () => {
const plugin = {};
const defaultReqVal = {
version: 'draft4',
body_schema: '{}',
};
const op1 = { requestBody: {} };
const op2 = { requestBody: { $ref: 'non-existent' } };
const op3 = {};
expect(generateRequestValidatorPlugin(plugin, op1).config).toStrictEqual(defaultReqVal);
expect(generateRequestValidatorPlugin(plugin, op2).config).toStrictEqual(defaultReqVal);
expect(generateRequestValidatorPlugin(plugin, op3).config).toStrictEqual(defaultReqVal);
});
it('should add non-json media types to allowed content types and not add body schema', () => {
const plugin = {};
const operation: OA3Operation = {
requestBody: {
content: {
'application/xml': {},
'text/yaml': {},
},
},
};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: '{}',
allowed_content_types: ['application/xml', 'text/yaml'],
});
});
it('should add body_schema and allowed content types', () => {
const plugin = {};
const schemaXml = {
type: 'Object',
properties: {
name: {
type: 'integer',
format: 'int64',
},
},
};
const schemaJson = {
type: 'Object',
properties: {
id: {
type: 'integer',
format: 'int64',
},
},
};
const operation: OA3Operation = {
requestBody: {
content: {
'application/xml': {
schema: schemaXml,
},
'application/json': {
schema: schemaJson,
},
},
},
};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: JSON.stringify(schemaJson),
allowed_content_types: ['application/xml', 'application/json'],
});
});
it('should default body_schema if no schema is defined or generated', () => {
const plugin = {};
const operation = {};
const generated = generateRequestValidatorPlugin(plugin, operation);
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: '{}',
});
});
});
});
});

View File

@ -34,52 +34,110 @@ export function generatePlugin(key: string, value: Object): DCPlugin {
return plugin;
}
export function generateRequestValidatorPlugin(obj: Object, operation: OA3Operation): DCPlugin {
const config: { [string]: Object } = {
version: 'draft4', // Fixed version
};
/*
This is valid config to allow all content to pass
See: https://github.com/Kong/kong-plugin-enterprise-request-validator/pull/34/files#diff-1a1d2d5ce801cc1cfb2aa91ae15686d81ef900af1dbef00f004677bc727bfd3cR284
*/
const ALLOW_ALL_SCHEMA = '{}';
config.parameter_schema = [];
function generateParameterSchema(operation: OA3Operation): Array<Object> | typeof undefined {
let parameterSchema;
if (operation.parameters) {
if (operation.parameters?.length) {
parameterSchema = [];
for (const p of operation.parameters) {
if (!(p: Object).schema) {
throw new Error("Parameter using 'content' type validation is not supported");
// The following is valid config to allow all content to pass, in the case where schema is not defined
let schema;
if ((p: Object).schema) {
schema = JSON.stringify((p: Object).schema);
} else if ((p: Object).content) {
// only parameters defined with a schema (not content) are supported
schema = ALLOW_ALL_SCHEMA;
} else {
// no schema or content property on a parameter is in violation with the OpenAPI spec
schema = ALLOW_ALL_SCHEMA;
}
config.parameter_schema.push({
parameterSchema.push({
in: (p: Object).in,
explode: !!(p: Object).explode,
required: !!(p: Object).required,
name: (p: Object).name,
schema: JSON.stringify((p: Object).schema),
schema,
style: (p: Object).style ?? 'form',
});
}
}
if (operation.requestBody) {
const content = (operation.requestBody: Object).content;
if (!content) {
throw new Error('content property is missing for request-validator!');
}
return parameterSchema;
}
let bodySchema;
for (const mediatype of Object.keys(content)) {
if (mediatype !== 'application/json') {
throw new Error(`Body validation supports only 'application/json', not ${mediatype}`);
}
const item = content[mediatype];
function generateBodyOptions(
operation: OA3Operation,
): {
bodySchema: string | typeof undefined,
allowedContentTypes: Array<string> | typeof undefined,
} {
let bodySchema;
let allowedContentTypes;
const bodyContent = (operation.requestBody: Object)?.content;
if (bodyContent) {
const jsonContentType = 'application/json';
allowedContentTypes = Object.keys(bodyContent);
if (allowedContentTypes.includes(jsonContentType)) {
const item = bodyContent[jsonContentType];
bodySchema = JSON.stringify(item.schema);
}
}
if (bodySchema) {
config.body_schema = bodySchema;
}
return { bodySchema, allowedContentTypes };
}
export function generateRequestValidatorPlugin(plugin: Object, operation: OA3Operation): DCPlugin {
const config: { [string]: Object } = {
version: 'draft4', // Fixed version
};
const pluginConfig = plugin.config ?? {};
// Use original or generated parameter_schema
const parameterSchema = pluginConfig.parameter_schema ?? generateParameterSchema(operation);
const generated = generateBodyOptions(operation);
// Use original or generated body_schema
let bodySchema = pluginConfig.body_schema ?? generated.bodySchema;
// If no parameter_schema or body_schema is defined or generated, allow all content to pass
if (parameterSchema === undefined && bodySchema === undefined) {
bodySchema = ALLOW_ALL_SCHEMA;
}
// Apply parameter_schema and body_schema to the config object
if (parameterSchema !== undefined) {
config.parameter_schema = parameterSchema;
}
if (bodySchema !== undefined) {
config.body_schema = bodySchema;
}
// Use original or generated allowed_content_types
const allowedContentTypes = pluginConfig.allowed_content_types ?? generated.allowedContentTypes;
if (allowedContentTypes !== undefined) {
config.allowed_content_types = allowedContentTypes;
}
// Use original verbose_response if defined
if (pluginConfig.verbose_response !== undefined) {
config.verbose_response = Boolean(pluginConfig.verbose_response);
}
return {
config,
enabled: true,
enabled: Boolean(plugin.enabled ?? true),
name: 'request-validator',
};
}

View File

@ -42,6 +42,7 @@ export function generateService(
for (const routePath of Object.keys(api.paths)) {
const pathItem: OA3PathItem = api.paths[routePath];
// TODO: Add path plugins to route
for (const method of Object.keys(pathItem)) {
if (
method !== 'get' &&

View File

@ -37,9 +37,13 @@ declare type OA3Parameter = {|
deprecated?: boolean,
allowEmptyValue?: boolean,
style?: 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject',
schema?: Object,
content?: Object,
explode?: boolean,
|};
declare type OA3RequestBody = {|
content?: Object,
// TODO
|};