/
openapi.request.validator.ts
232 lines (209 loc) 路 7.16 KB
/
openapi.request.validator.ts
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
import { Ajv } from 'ajv';
import { createRequestAjv } from '../framework/ajv';
import {
ContentType,
ajvErrorsToValidatorError,
augmentAjvErrors,
} from './util';
import { NextFunction, RequestHandler, Response } from 'express';
import {
ValidationSchema,
OpenAPIV3,
OpenApiRequest,
RequestValidatorOptions,
ValidateRequestOpts,
OpenApiRequestMetadata,
NotFound,
MethodNotAllowed,
BadRequest,
} from '../framework/types';
import { BodySchemaParser } from './parsers/body.parse';
import { ParametersSchemaParser } from './parsers/schema.parse';
import { RequestParameterMutator } from './parsers/req.parameter.mutator';
type OperationObject = OpenAPIV3.OperationObject;
type SchemaObject = OpenAPIV3.SchemaObject;
type ReferenceObject = OpenAPIV3.ReferenceObject;
type SecurityRequirementObject = OpenAPIV3.SecurityRequirementObject;
type SecuritySchemeObject = OpenAPIV3.SecuritySchemeObject;
type ApiKeySecurityScheme = OpenAPIV3.ApiKeySecurityScheme;
export class RequestValidator {
private middlewareCache: { [key: string]: RequestHandler } = {};
private apiDoc: OpenAPIV3.Document;
private ajv: Ajv;
private requestOpts: ValidateRequestOpts = {};
constructor(
apiDoc: OpenAPIV3.Document,
options: RequestValidatorOptions = {},
) {
this.middlewareCache = {};
this.apiDoc = apiDoc;
this.requestOpts.allowUnknownQueryParameters =
options.allowUnknownQueryParameters;
this.ajv = createRequestAjv(apiDoc, options);
}
public validate(
req: OpenApiRequest,
res: Response,
next: NextFunction,
): void {
if (!req.openapi) {
// this path was not found in open api and
// this path is not defined under an openapi base path
// skip it
return next();
}
const openapi = <OpenApiRequestMetadata>req.openapi;
const path = openapi.expressRoute;
if (!path) {
throw new NotFound({
path: req.path,
message: 'not found',
});
}
const reqSchema = openapi.schema;
if (!reqSchema) {
throw new MethodNotAllowed({
path: req.path,
message: `${req.method} method not allowed`,
});
}
// cache middleware by combining method, path, and contentType
const contentType = ContentType.from(req);
const contentTypeKey = contentType.equivalents()[0] ?? 'not_provided';
// use openapi.expressRoute as path portion of key
const key = `${req.method}-${path}-${contentTypeKey}`;
if (!this.middlewareCache[key]) {
const middleware = this.buildMiddleware(path, reqSchema, contentType);
this.middlewareCache[key] = middleware;
}
return this.middlewareCache[key](req, res, next);
}
private buildMiddleware(
path: string,
reqSchema: OperationObject,
contentType: ContentType,
): RequestHandler {
const apiDoc = this.apiDoc;
const schemaParser = new ParametersSchemaParser(this.ajv, apiDoc);
const bodySchemaParser = new BodySchemaParser(this.ajv, apiDoc);
const parameters = schemaParser.parse(path, reqSchema.parameters);
const securityQueryParam = Security.queryParam(apiDoc, reqSchema);
const body = bodySchemaParser.parse(path, reqSchema, contentType);
const isBodyBinary = body?.['format'] === 'binary';
const properties: ValidationSchema = {
...parameters,
body: isBodyBinary ? {} : body,
};
// TODO throw 400 if missing a required binary body
const required =
(<SchemaObject>body).required && !isBodyBinary ? ['body'] : [];
// $schema: "http://json-schema.org/draft-04/schema#",
const schema = {
required: ['query', 'headers', 'params'].concat(required),
properties,
};
const validator = this.ajv.compile(schema);
return (req: OpenApiRequest, res: Response, next: NextFunction): void => {
const openapi = <OpenApiRequestMetadata>req.openapi;
const hasPathParams = Object.keys(openapi.pathParams).length > 0;
if (hasPathParams) {
req.params = openapi.pathParams ?? req.params;
}
const mutator = new RequestParameterMutator(
this.ajv,
apiDoc,
path,
properties,
);
mutator.modifyRequest(req);
if (!this.requestOpts.allowUnknownQueryParameters) {
this.processQueryParam(
req.query,
schema.properties.query,
securityQueryParam,
);
}
const cookies = req.cookies
? {
...req.cookies,
...req.signedCookies,
}
: undefined;
const valid = validator({ ...req, cookies });
if (valid) {
next();
} else {
const errors = augmentAjvErrors([...(validator.errors ?? [])]);
const err = ajvErrorsToValidatorError(400, errors);
const message = this.ajv.errorsText(errors, { dataVar: 'request' });
const error: BadRequest = new BadRequest({
path: req.path,
message: message,
});
error.errors = err.errors;
throw error;
}
};
}
private processQueryParam(query, schema, whiteList: string[] = []) {
const keys = schema.properties ? Object.keys(schema.properties) : [];
const knownQueryParams = new Set(keys);
whiteList.forEach((item) => knownQueryParams.add(item));
const queryParams = Object.keys(query);
const allowedEmpty = schema.allowEmptyValue;
for (const q of queryParams) {
if (
!this.requestOpts.allowUnknownQueryParameters &&
!knownQueryParams.has(q)
) {
throw new BadRequest({
path: `.query.${q}`,
message: `Unknown query parameter '${q}'`,
});
} else if (!allowedEmpty?.has(q) && (query[q] === '' || null)) {
throw new BadRequest({
path: `.query.${q}`,
message: `Empty value found for query parameter '${q}'`,
});
}
}
}
}
class Security {
public static queryParam(
apiDocs: OpenAPIV3.Document,
schema: OperationObject,
): string[] {
const hasPathSecurity =
schema.hasOwnProperty('security') && schema.security.length > 0;
const hasRootSecurity =
apiDocs.hasOwnProperty('security') && apiDocs.security.length > 0;
let usedSecuritySchema: SecurityRequirementObject[] = [];
if (hasPathSecurity) {
usedSecuritySchema = schema.security;
} else if (hasRootSecurity) {
// if no security schema for the path, use top-level security schema
usedSecuritySchema = apiDocs.security;
}
const securityQueryParameter = this.getSecurityQueryParams(
usedSecuritySchema,
apiDocs.components?.securitySchemes,
);
return securityQueryParameter;
}
private static getSecurityQueryParams(
usedSecuritySchema: SecurityRequirementObject[],
securitySchema: { [key: string]: ReferenceObject | SecuritySchemeObject },
): string[] {
return usedSecuritySchema && securitySchema
? usedSecuritySchema
.filter((obj) => Object.entries(obj).length !== 0)
.map((sec) => {
const securityKey = Object.keys(sec)[0];
return <SecuritySchemeObject>securitySchema[securityKey];
})
.filter((sec) => sec?.type === 'apiKey' && sec?.in == 'query')
.map((sec: ApiKeySecurityScheme) => sec.name)
: [];
}
}