Skip to content

Commit

Permalink
Merge branch 'master' into fix
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Nov 2, 2019
2 parents f4ba7f0 + 431b4b4 commit e137630
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 69 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ Determines whether the validator should validate responses. Also accepts respons

For example:

```json
```javascript
validateResponses: {
removeAdditional: 'failing'
}
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"nodemon": "^1.19.1",
"nyc": "^14.1.1",
"prettier": "^1.18.2",
"source-map-support": "0.5.13",
"source-map-support": "0.5.14",
"supertest": "^4.0.2",
"ts-node": "^8.3.0",
"tsc": "^1.20150623.0",
Expand Down
22 changes: 22 additions & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ type OpenAPIErrorTransformer = ({}, {}) => object;

type PathSecurityTuple = [RegExp, SecurityRequirement[]];

export type SecurityHandlers = {
[key: string]: (
req: Request,
scopes: string[],
schema: OpenAPIV3.SecuritySchemeObject,
) => boolean | Promise<boolean>;
};

export type ValidateResponseOpts = {
removeAdditional?: string | boolean;
};

export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
validateResponses?: boolean | ValidateResponseOpts;
validateRequests?: boolean;
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean;
unknownFormats?: string[] | string | boolean;
multerOpts?: {};
}

interface SecurityRequirement {
[name: string]: SecurityScope[];
}
Expand Down
115 changes: 54 additions & 61 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
import ono from 'ono';
import * as _ from 'lodash';
import {
Application,
Request,
Response,
NextFunction,
RequestHandler,
} from 'express';
import * as middlewares from './middlewares';
import { Application, Response, NextFunction } from 'express';
import { OpenApiContext } from './framework/openapi.context';
import {
OpenAPIV3,
OpenApiValidatorOpts,
ValidateResponseOpts,
OpenApiRequest,
OpenApiRequestHandler,
} from './framework/types';
import * as middlewares from './middlewares';

export type SecurityHandlers = {
[key: string]: (
req: Request,
scopes: string[],
schema: OpenAPIV3.SecuritySchemeObject,
) => boolean | Promise<boolean>;
};
export type ValidateResponseOpts = {
removeAdditional?: string | boolean;
};
export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
validateResponses?: boolean | ValidateResponseOpts;
validateRequests?: boolean;
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean;
unknownFormats?: string[] | string | boolean;
multerOpts?: {};
}

export class OpenApiValidator {
private app: Application;
private context: OpenApiContext;
private options: OpenApiValidatorOpts;

Expand All @@ -46,28 +22,40 @@ export class OpenApiValidator {
if (options.coerceTypes == null) options.coerceTypes = true;
if (options.validateRequests == null) options.validateRequests = true;
if (options.validateResponses == null) options.validateResponses = false;
if (!options.validateRequests) throw Error('validateRequests must be true');

if (!options.validateResponses) {
} else if (
options.validateResponses === true ||
options.validateResponses === 'strict'
) {
if (options.validateResponses === true) {
options.validateResponses = {
removeAdditional: false,
};
}

this.options = options;

const openApiContext = new OpenApiContext({
this.context = new OpenApiContext({
apiDoc: options.apiSpec,
});
}

public install(app: Application): void {
this.app = app;
this.installPathParams();
this.installMetadataMiddleware();
this.installMultipartMiddleware();

this.context = openApiContext;
const components = this.context.apiDoc.components;
if (components && components.securitySchemes) {
this.installSecurityMiddleware();
}

if (this.options.validateRequests) {
this.installRequestValidationMiddleware();
}

if (this.options.validateResponses) {
this.installResponseValidationMiddleware();
}
}

install(app: Application) {
private installPathParams(): void {
const pathParams = [];
for (const route of this.context.routes) {
if (route.pathParams.length > 0) {
Expand All @@ -77,7 +65,7 @@ export class OpenApiValidator {

// install param on routes with paths
for (const p of _.uniq(pathParams)) {
app.param(
this.app.param(
p,
(
req: OpenApiRequest,
Expand All @@ -94,7 +82,25 @@ export class OpenApiValidator {
},
);
}
}

private installMetadataMiddleware(): void {
this.app.use(middlewares.applyOpenApiMetadata(this.context));
}

private installMultipartMiddleware(): void {
this.app.use(middlewares.multipart(this.context, this.options.multerOpts));
}

private installSecurityMiddleware(): void {
const securityMiddleware = middlewares.security(
this.context,
this.options.securityHandlers,
);
this.app.use(securityMiddleware);
}

private installRequestValidationMiddleware(): void {
const { coerceTypes, unknownFormats } = this.options;
const requestValidator = new middlewares.RequestValidator(
this.context.apiDoc,
Expand All @@ -106,13 +112,15 @@ export class OpenApiValidator {
unknownFormats,
},
);

const requestValidatorMw: OpenApiRequestHandler = (req, res, next) =>
const requestValidationHandler: OpenApiRequestHandler = (req, res, next) =>
requestValidator.validate(req, res, next);

const removeAdditional =
this.options.validateResponses &&
(<ValidateResponseOpts>this.options.validateResponses).removeAdditional;
this.app.use(requestValidationHandler);
}

private installResponseValidationMiddleware(): void {
const { coerceTypes, unknownFormats, validateResponses } = this.options;
const { removeAdditional } = <ValidateResponseOpts>validateResponses;

const responseValidator = new middlewares.ResponseValidator(
this.context.apiDoc,
Expand All @@ -123,22 +131,7 @@ export class OpenApiValidator {
},
);

const securityMiddleware = middlewares.security(
this.context,
this.options.securityHandlers,
);

const components = this.context.apiDoc.components;
const use = [
middlewares.applyOpenApiMetadata(this.context),
middlewares.multipart(this.context, this.options.multerOpts),
];
// TODO validate security functions exist for each security key
if (components && components.securitySchemes) use.push(securityMiddleware);
if (this.options.validateRequests) use.push(requestValidatorMw);
if (this.options.validateResponses) use.push(responseValidator.validate());

app.use(use);
this.app.use(responseValidator.validate());
}

private validateOptions(options: OpenApiValidatorOpts): void {
Expand Down
11 changes: 8 additions & 3 deletions src/middlewares/openapi.security.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { SecurityHandlers } from '../index';
import { OpenAPIV3, OpenApiRequest } from '../framework/types';
import {
OpenAPIV3,
OpenApiRequest,
SecurityHandlers,
} from '../framework/types';
import { validationError } from './util';
import { OpenApiContext } from '../framework/openapi.context';

Expand Down Expand Up @@ -36,7 +39,9 @@ export function security(
if (!pathSchema) {
// add openapi metadata to make this case more clear
// its not obvious that missig schema means methodNotAllowed
return next(validationError(405, req.path, `${req.method} method not allowed`));
return next(
validationError(405, req.path, `${req.method} method not allowed`),
);
}

// use the local security object or fallbac to api doc's security or undefined
Expand Down

0 comments on commit e137630

Please sign in to comment.