From 5a6b330c52b1b00559e5e29c73bb4c379df611b3 Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Sun, 6 Oct 2019 20:37:47 -0400 Subject: [PATCH] initial security integration --- src/framework/openapi.spec.loader.ts | 23 ++++++-- src/framework/types.ts | 1 - src/index.ts | 22 ++++++-- src/middlewares/index.ts | 1 + src/middlewares/openapi.response.validator.ts | 24 --------- src/middlewares/openapi.security.ts | 22 ++++++++ test/resources/security.yaml | 34 ++++++++++++ test/security.spec.ts | 54 +++++++++++++++++++ 8 files changed, 148 insertions(+), 33 deletions(-) create mode 100644 src/middlewares/openapi.security.ts create mode 100644 test/resources/security.yaml create mode 100644 test/security.spec.ts diff --git a/src/framework/openapi.spec.loader.ts b/src/framework/openapi.spec.loader.ts index 5f6cd52d..5a271124 100644 --- a/src/framework/openapi.spec.loader.ts +++ b/src/framework/openapi.spec.loader.ts @@ -44,6 +44,7 @@ export class OpenApiSpecLoader { framework.initialize({ visitApi(ctx: OpenAPIFrameworkAPIContext) { const apiDoc = ctx.getApiDoc(); + const security = apiDoc.security; for (const bpa of basePaths) { const bp = bpa.replace(/\/$/, ''); for (const [path, methods] of Object.entries(apiDoc.paths)) { @@ -52,8 +53,12 @@ export class OpenApiSpecLoader { continue; } const schemaParameters = new Set(); - (schema.parameters || []).forEach(parameter => schemaParameters.add(parameter)); - ((methods as any).parameters || []).forEach(parameter => schemaParameters.add(parameter)); + (schema.parameters || []).forEach(parameter => + schemaParameters.add(parameter), + ); + ((methods as any).parameters || []).forEach(parameter => + schemaParameters.add(parameter), + ); schema.parameters = Array.from(schemaParameters); const pathParams = new Set(); for (const param of schema.parameters) { @@ -66,12 +71,24 @@ export class OpenApiSpecLoader { .split('/') .map(toExpressParams) .join('/'); + + // add apply any general defined security + const moddedSchema = + security || schema.security + ? { + schema, + security: [ + ...(security || []), + ...(schema.security || []), + ], + } + : { ...schema }; routes.push({ expressRoute, openApiRoute, method: method.toUpperCase(), pathParams: Array.from(pathParams), - schema, + schema: moddedSchema, }); } } diff --git a/src/framework/types.ts b/src/framework/types.ts index 861d2270..fe8c8551 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -375,7 +375,6 @@ export interface OpenAPIFrameworkPathContext { export interface OpenAPIFrameworkVisitor { visitApi?(context: OpenAPIFrameworkAPIContext): void; visitPath?(context: OpenAPIFrameworkPathContext): void; - // visitOperation?(context: OpenAPIFrameworkOperationContext): void; } export interface OpenApiRequest extends Request { diff --git a/src/index.ts b/src/index.ts index de9d61e2..6acc7d9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import ono from 'ono'; import * as _ from 'lodash'; -import { Application } from 'express'; +import { Application, Request } from 'express'; import { OpenApiContext } from './framework/openapi.context'; import { OpenAPIV3, OpenApiRequest } from './framework/types'; import * as middlewares from './middlewares'; @@ -10,6 +10,13 @@ export interface OpenApiValidatorOpts { coerceTypes?: boolean; validateResponses?: boolean; validateRequests?: boolean; + securityHandlers?: { + [key: string]: ( + req: Request, + scopes: [], + schema: OpenAPIV3.SecuritySchemeObject, + ) => boolean | Promise; + }; multerOpts?: {}; } @@ -58,20 +65,25 @@ export class OpenApiValidator { useDefaults: true, }); - const validateMiddleware = (req, res, next) => { + const requestValidator = (req, res, next) => { return aoav.validate(req, res, next); }; - const resOav = new middlewares.ResponseValidator(this.context.apiDoc, { + const responseValidator = new middlewares.ResponseValidator(this.context.apiDoc, { coerceTypes, }); + const securityMiddleware = middlewares.security(this.context); + + const components = this.context.apiDoc.components; const use = [ middlewares.applyOpenApiMetadata(this.context), middlewares.multipart(this.context, this.options.multerOpts), ]; - if (this.options.validateRequests) use.push(validateMiddleware); - if (this.options.validateResponses) use.push(resOav.validate()); + // TODO validate security functions exist for each security key + if (components && components.securitySchemes) use.push(securityMiddleware); + if (this.options.validateRequests) use.push(requestValidator); + if (this.options.validateResponses) use.push(responseValidator.validate()); app.use(use); } diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts index 9c58458b..5eed347c 100644 --- a/src/middlewares/index.ts +++ b/src/middlewares/index.ts @@ -2,3 +2,4 @@ export { applyOpenApiMetadata } from './openapi.metadata'; export { RequestValidator } from './openapi.request.validator'; export { ResponseValidator } from './openapi.response.validator'; export { multipart } from './openapi.multipart'; +export { security } from './openapi.security'; \ No newline at end of file diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index ab718c40..c613c005 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -129,28 +129,4 @@ export class ResponseValidator { } return validators; } - - private validateBody(body) {} - - private toOpenapiValidationError(error: Ajv.ErrorObject) { - const validationError = { - path: `instance${error.dataPath}`, - errorCode: `${error.keyword}.openapi.responseValidation`, - message: error.message, - }; - - validationError.path = validationError.path.replace( - /^instance\.(?:response\.)?/, - '', - ); - - validationError.message = - validationError.path + ' ' + validationError.message; - - if (validationError.path === 'response') { - delete validationError.path; - } - - return validationError; - } } diff --git a/src/middlewares/openapi.security.ts b/src/middlewares/openapi.security.ts new file mode 100644 index 00000000..e3b55e73 --- /dev/null +++ b/src/middlewares/openapi.security.ts @@ -0,0 +1,22 @@ +import { OpenApiContext } from '../framework/openapi.context'; +// import { validationError } from './util'; + +export function security(openApiContext: OpenApiContext) { + return (req, res, next) => { + 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 security = req.openapi.schema.security; + if (!security) { + return next(); + } + + console.log('found security ', security, req.openapi); + // run security handlers + next(); + }; +} diff --git a/test/resources/security.yaml b/test/resources/security.yaml new file mode 100644 index 00000000..f4168fe8 --- /dev/null +++ b/test/resources/security.yaml @@ -0,0 +1,34 @@ +openapi: '3.0.2' +info: + version: 1.0.0 + title: requestBodies $ref + description: requestBodies $ref Test + +servers: + - url: /v1/ + +paths: + /api_key: + get: + security: + - ApiKeyAuth: [] + responses: + '200': + description: OK + '400': + description: Bad Request + #'401': + # description: unauthorized + +components: + securitySchemes: + BasicAuth: + type: http + scheme: basic + BearerAuth: + type: http + scheme: bearer + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key diff --git a/test/security.spec.ts b/test/security.spec.ts new file mode 100644 index 00000000..25d15b86 --- /dev/null +++ b/test/security.spec.ts @@ -0,0 +1,54 @@ +import * as path from 'path'; +import * as express from 'express'; +import { expect } from 'chai'; +import * as request from 'supertest'; +import { createApp } from './common/app'; + +const packageJson = require('../package.json'); + +describe(packageJson.name, () => { + let app = null; + let basePath = null; + + before(async () => { + // Set up the express app + const apiSpec = path.join('test', 'resources', 'security.yaml'); + app = await createApp( + { + apiSpec, + securityHandlers: { + ApiKeyAuth: function(req, scopes, schema) { + console.log('-------in sec handler'); + }, + }, + }, + 3005, + ); + basePath = app.basePath; + console.log(basePath); + + app.use( + `${basePath}`, + express + .Router() + .get(`/api_key`, (req, res) => res.json({ logged_in: true })), + ); + }); + + after(() => { + app.server.close(); + }); + + it.only('should return 401 if apikey not valid', async () => + request(app) + .get(`${basePath}/api_key`) + .send({}) + .expect(401) + .then(r => { + console.log(r.body); + expect(r.body.errors).to.be.an('array'); + expect(r.body.errors).to.have.length(1); + + // TODO add test + })); +});