├── .editorconfig ├── README.md ├── package-lock.json ├── package.json ├── src └── index.js └── test ├── fixtures ├── externalValidatorFunction.yaml ├── externalValidatorId.yaml ├── invalidValidatorFunction.yaml ├── localValidatorFunction.yaml └── noValidatorFunction.yaml └── schema.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = crlf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false 10 | 11 | [.md] 12 | indent_style = space 13 | indent_size = 4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-reqvalidator-plugin 2 | Serverless plugin to set specific validator request on method 3 | 4 | ## Installation 5 | ``` 6 | npm install serverless-reqvalidator-plugin 7 | ``` 8 | 9 | ## Requirements 10 | This require you to have documentation plugin installed 11 | ``` 12 | serverless-aws-documentation 13 | ``` 14 | 15 | 16 | ## Using plugin 17 | Specify plugin 18 | ``` 19 | plugins: 20 | - serverless-reqvalidator-plugin 21 | - serverless-aws-documentation 22 | ``` 23 | 24 | 25 | In `serverless.yml` create custom resource for request validators 26 | 27 | ``` 28 | xMyRequestValidator: 29 | Type: "AWS::ApiGateway::RequestValidator" 30 | Properties: 31 | Name: 'my-req-validator' 32 | RestApiId: 33 | Ref: ApiGatewayRestApi 34 | ValidateRequestBody: true 35 | ValidateRequestParameters: false 36 | ``` 37 | 38 | For every function you wish to use the validator set property `reqValidatorName: 'xMyRequestValidator'` to match resource you described 39 | 40 | ``` 41 | debug: 42 | handler: apis/admin/debug/debug.debug 43 | timeout: 10 44 | events: 45 | - http: 46 | path: admin/debug 47 | method: get 48 | cors: true 49 | private: true 50 | reqValidatorName: 'xMyRequestValidator' 51 | ``` 52 | 53 | ### Use Validator specified in different Stack 54 | The serverless framework allows us to share resources among several stacks. Therefore a CloudFormation Output has to be specified in one stack. This Output can be imported in another stack to make use of it. For more information see 55 | [here](https://serverless.com/framework/docs/providers/aws/guide/variables/#reference-cloudformation-outputs). 56 | 57 | Specify a request validator in a different stack: 58 | 59 | ``` 60 | plugins: 61 | - serverless-reqvalidator-plugin 62 | service: my-service-a 63 | functions: 64 | hello: 65 | handler: handler.myHandler 66 | events: 67 | - http: 68 | path: hello 69 | reqValidatorName: 'myReqValidator' 70 | 71 | resources: 72 | Resources: 73 | xMyRequestValidator: 74 | Type: "AWS::ApiGateway::RequestValidator" 75 | Properties: 76 | Name: 'my-req-validator' 77 | RestApiId: 78 | Ref: ApiGatewayRestApi 79 | ValidateRequestBody: true 80 | ValidateRequestParameters: false 81 | Outputs: 82 | xMyRequestValidator: 83 | Value: 84 | Ref: my-req-validator 85 | Export: 86 | Name: myReqValidator 87 | 88 | 89 | ``` 90 | 91 | Make use of the exported request validator in stack b: 92 | ``` 93 | plugins: 94 | - serverless-reqvalidator-plugin 95 | service: my-service-b 96 | functions: 97 | hello: 98 | handler: handler.myHandler 99 | events: 100 | - http: 101 | path: hello 102 | reqValidatorName: 103 | Fn::ImportValue: 'myReqValidator' 104 | ``` 105 | 106 | ### Use an external validator by ID 107 | If you have an existing request validator defined outside of CloudFormation e.g. Terraform, you can reference it by id. 108 | 109 | ``` 110 | plugins: 111 | - serverless-reqvalidator-plugin 112 | service: my-service-a 113 | functions: 114 | hello: 115 | handler: handler.myHandler 116 | events: 117 | - http: 118 | path: hello 119 | reqValidatorName: 120 | id: 'g5ch0h' 121 | ``` 122 | 123 | ### Full example 124 | ``` 125 | service: 126 | name: my-service 127 | 128 | plugins: 129 | - serverless-webpack 130 | - serverless-reqvalidator-plugin 131 | - serverless-aws-documentation 132 | 133 | provider: 134 | name: aws 135 | runtime: nodejs6.10 136 | region: eu-west-2 137 | environment: 138 | NODE_ENV: ${self:provider.stage} 139 | custom: 140 | documentation: 141 | api: 142 | info: 143 | version: '1.0.0' 144 | title: My API 145 | description: This is my API 146 | tags: 147 | - 148 | name: User 149 | description: User Management 150 | models: 151 | - name: MessageResponse 152 | contentType: "application/json" 153 | schema: 154 | type: object 155 | properties: 156 | message: 157 | type: string 158 | - name: RegisterUserRequest 159 | contentType: "application/json" 160 | schema: 161 | required: 162 | - email 163 | - password 164 | properties: 165 | email: 166 | type: string 167 | password: 168 | type: string 169 | - name: RegisterUserResponse 170 | contentType: "application/json" 171 | schema: 172 | type: object 173 | properties: 174 | result: 175 | type: string 176 | - name: 400JsonResponse 177 | contentType: "application/json" 178 | schema: 179 | type: object 180 | properties: 181 | message: 182 | type: string 183 | statusCode: 184 | type: number 185 | commonModelSchemaFragments: 186 | MethodResponse400Json: 187 | statusCode: '400' 188 | responseModels: 189 | "application/json": 400JsonResponse 190 | 191 | functions: 192 | signUp: 193 | handler: handler.signUp 194 | events: 195 | - http: 196 | documentation: 197 | summary: "Register user" 198 | description: "Registers new user" 199 | tags: 200 | - User 201 | requestModels: 202 | "application/json": RegisterUserRequest 203 | method: post 204 | path: signup 205 | reqValidatorName: onlyBody 206 | methodResponses: 207 | - statusCode: '200' 208 | responseModels: 209 | "application/json": RegisterUserResponse 210 | - ${self:custom.commonModelSchemaFragments.MethodResponse400Json} 211 | 212 | package: 213 | include: 214 | handler.ts 215 | 216 | resources: 217 | Resources: 218 | onlyBody: 219 | Type: "AWS::ApiGateway::RequestValidator" 220 | Properties: 221 | Name: 'only-body' 222 | RestApiId: 223 | Ref: ApiGatewayRestApi 224 | ValidateRequestBody: true 225 | ValidateRequestParameters: false 226 | ``` 227 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-reqvalidator-plugin", 3 | "version": "3.1.0", 4 | "description": "Serverless plugin for setting request validation", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "jest test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/RafPe/serverless-reqvalidator-plugin.git" 12 | }, 13 | "keywords": [ 14 | "serverless", 15 | "plugin", 16 | "requestvalidator" 17 | ], 18 | "author": "RafPe < me@rafpe.ninja >", 19 | "contributors": [ 20 | "pj035 pm.jaecks@gmail.com (http://pjaecks.de)" 21 | ], 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/RafPe/serverless-reqvalidator-plugin/issues" 25 | }, 26 | "homepage": "https://github.com/RafPe/serverless-reqvalidator-plugin#readme", 27 | "devDependencies": { 28 | "ajv": "^8.11.2", 29 | "jest": "^29.3.1", 30 | "js-yaml": "^4.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Adds using of custom created request validator on specific functions by 5 | * adding `reqValidatorName` 6 | * 7 | * Usage: 8 | * 9 | * myFuncGetItem: 10 | * handler: myFunc.get 11 | * name: ${self:provider.stage}-myFunc-get-item 12 | * events: 13 | * - http: 14 | * method: GET 15 | * path: mypath 16 | * cors: true 17 | * reqValidatorName: 'xyz' 18 | * 19 | * Alternative usage: 20 | * 21 | * myFuncGetItem: 22 | * handler: myFunc.get 23 | * name: ${self:provider.stage}-myFunc-get-item 24 | * events: 25 | * - http: 26 | * method: GET 27 | * path: mypath 28 | * cors: true 29 | * reqValidatorName: 30 | * Fn::ImportValue: 'my-import-value' 31 | * 32 | * Use request validator by Id: 33 | * 34 | * myFuncGetItem: 35 | * handler: myFunc.get 36 | * name: ${self:provider.stage}-myFunc-get-item 37 | * events: 38 | * - http: 39 | * method: GET 40 | * path: mypath 41 | * cors: true 42 | * reqValidatorName: 43 | * id: 'g5ch0h' 44 | * 45 | * 46 | * 47 | * Resources used: 48 | * - https://www.snip2code.com/Snippet/1467589/adds-the-posibility-to-configure-AWS_IAM/ 49 | */ 50 | 51 | const ServerlessReqValidatorPluginConfigSchema = { 52 | properties: { 53 | reqValidatorName: { 54 | anyOf: [ 55 | { type: 'string' }, 56 | { type: 'object', 57 | properties: { 58 | 'Fn::ImportValue': { type: 'string' } 59 | }, 60 | required: ['Fn::ImportValue'], 61 | }, 62 | { type: 'object', 63 | properties: { 64 | id: { type: 'string' }, 65 | }, 66 | required: ['id'], 67 | }, 68 | ] 69 | }, 70 | }, 71 | } 72 | class ServerlessReqValidatorPlugin { 73 | constructor(serverless, options) { 74 | this.serverless = serverless; 75 | this.options = options; 76 | 77 | this.provider = this.serverless.getProvider('aws'); 78 | const naming = this.serverless.providers.aws.naming; 79 | 80 | this.getMethodLogicalId = naming.getMethodLogicalId.bind(naming); 81 | this.normalizePath = naming.normalizePath.bind(naming); 82 | 83 | this._beforeDeploy = this.beforeDeploy.bind(this) 84 | 85 | // Create schema for your properties. For reference use https://github.com/ajv-validator/ajv 86 | serverless.configSchemaHandler.defineFunctionEventProperties('aws', 'http', ServerlessReqValidatorPluginConfigSchema); 87 | 88 | this.hooks = { 89 | 'before:package:finalize': this._beforeDeploy 90 | }; 91 | 92 | } 93 | 94 | beforeDeploy() { 95 | 96 | const resources = this.serverless.service.provider.compiledCloudFormationTemplate.Resources 97 | 98 | this.serverless.service.getAllFunctions().forEach((functionName) => { 99 | const functionObject = this.serverless.service.functions[functionName]; 100 | 101 | functionObject.events.forEach(event => { 102 | 103 | if (!event.http) { return; } 104 | 105 | const reqValidatorName = event.http.reqValidatorName; 106 | if (reqValidatorName) { 107 | 108 | let path; 109 | let method; 110 | 111 | if (typeof event.http === 'object') { 112 | path = event.http.path; 113 | method = event.http.method; 114 | } else if (typeof event.http === 'string') { 115 | path = event.http.split(' ')[1]; 116 | method = event.http.split(' ')[0]; 117 | } 118 | 119 | const resourcesArray = path.split('/'); 120 | // resource name is the last element in the endpoint. It's not unique. 121 | const resourceName = path.split('/')[path.split('/').length - 1]; 122 | const normalizedResourceName = resourcesArray.map(this.normalizePath).join(''); 123 | const normalizedMethod = method[0].toUpperCase() + method.substr(1).toLowerCase(); 124 | const methodName = `ApiGatewayMethod${normalizedResourceName}${normalizedMethod}`; 125 | 126 | switch (typeof reqValidatorName) { 127 | case 'object': 128 | if (reqValidatorName['Fn::ImportValue']) { 129 | resources[methodName].Properties.RequestValidatorId = reqValidatorName; 130 | } else if (reqValidatorName['id']) { 131 | resources[methodName].Properties.RequestValidatorId = reqValidatorName['id'] 132 | } else { // other use cases should be added here 133 | resources[methodName].Properties.RequestValidatorId = reqValidatorName; 134 | } 135 | break; 136 | case 'string': 137 | default: 138 | resources[methodName].Properties.RequestValidatorId = { "Ref": `${reqValidatorName}` }; 139 | break; 140 | } 141 | } 142 | }); 143 | } 144 | ) 145 | } 146 | 147 | } 148 | 149 | module.exports = ServerlessReqValidatorPlugin; 150 | module.exports.ServerlessReqValidatorPluginConfigSchema = ServerlessReqValidatorPluginConfigSchema; 151 | -------------------------------------------------------------------------------- /test/fixtures/externalValidatorFunction.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - serverless-reqvalidator-plugin 3 | service: my-service-b 4 | functions: 5 | hello: 6 | handler: handler.myHandler 7 | events: 8 | - http: 9 | path: hello 10 | reqValidatorName: 11 | Fn::ImportValue: 'myReqValidator' 12 | -------------------------------------------------------------------------------- /test/fixtures/externalValidatorId.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - serverless-reqvalidator-plugin 3 | service: my-service-b 4 | functions: 5 | hello: 6 | handler: handler.myHandler 7 | events: 8 | - http: 9 | path: hello 10 | reqValidatorName: 11 | id: 'abc123' 12 | -------------------------------------------------------------------------------- /test/fixtures/invalidValidatorFunction.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - serverless-reqvalidator-plugin 3 | service: my-service-d 4 | functions: 5 | hello: 6 | handler: handler.myHandler 7 | events: 8 | - http: 9 | path: hello 10 | reqValidatorName: 11 | data: 123 12 | -------------------------------------------------------------------------------- /test/fixtures/localValidatorFunction.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - serverless-reqvalidator-plugin 3 | service: my-service-a 4 | functions: 5 | hello: 6 | handler: handler.myHandler 7 | events: 8 | - http: 9 | path: hello 10 | reqValidatorName: 'myReqValidator' 11 | 12 | resources: 13 | Resources: 14 | xMyRequestValidator: 15 | Type: "AWS::ApiGateway::RequestValidator" 16 | Properties: 17 | Name: 'my-req-validator' 18 | RestApiId: 19 | Ref: ApiGatewayRestApi 20 | ValidateRequestBody: true 21 | ValidateRequestParameters: false 22 | Outputs: 23 | xMyRequestValidator: 24 | Value: 25 | Ref: my-req-validator 26 | Export: 27 | Name: myReqValidator 28 | -------------------------------------------------------------------------------- /test/fixtures/noValidatorFunction.yaml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - serverless-reqvalidator-plugin 3 | service: my-service-c 4 | functions: 5 | hello: 6 | handler: handler.myHandler 7 | events: 8 | - http: 9 | path: hello 10 | -------------------------------------------------------------------------------- /test/schema.test.js: -------------------------------------------------------------------------------- 1 | const Ajv = require('ajv'); 2 | const yaml = require('js-yaml'); 3 | const fs = require('fs'); 4 | 5 | const { ServerlessReqValidatorPluginConfigSchema } = require('../src/index'); 6 | 7 | const ajv = new Ajv(); 8 | 9 | const validateConfig = ajv.compile({ 10 | type: 'object', 11 | ...ServerlessReqValidatorPluginConfigSchema, 12 | }); 13 | 14 | describe('serverless-reqvalidator-plugin schema', () => { 15 | it('should validate a configuration without reqValidatorName property', () => { 16 | const config = yaml.load(fs.readFileSync('./test/fixtures/noValidatorFunction.yaml', 'utf8')); 17 | 18 | const valid = validateConfig(config.functions.hello.events[0].http); 19 | expect(valid).toBeTruthy(); 20 | }); 21 | 22 | it('should validate a configuration with a local reqValidatorName property', () => { 23 | const config = yaml.load(fs.readFileSync('./test/fixtures/localValidatorFunction.yaml', 'utf8')); 24 | 25 | const valid = validateConfig(config.functions.hello.events[0].http); 26 | expect(valid).toBeTruthy(); 27 | }); 28 | 29 | it('should validate a configuration with an external reqValidatorName property', () => { 30 | const config = yaml.load(fs.readFileSync('./test/fixtures/externalValidatorFunction.yaml', 'utf8')); 31 | 32 | const valid = validateConfig(config.functions.hello.events[0].http); 33 | expect(valid).toBeTruthy(); 34 | }); 35 | 36 | it('should not validate a configuration with an invalid reqValidatorName property', () => { 37 | const config = yaml.load(fs.readFileSync('./test/fixtures/invalidValidatorFunction.yaml', 'utf8')); 38 | const valid = validateConfig(config.functions.hello.events[0].http); 39 | expect(valid).toBeFalsy(); 40 | }); 41 | 42 | it('should validate a configuration with an external reqValidatorName.id property', () => { 43 | const config = yaml.load(fs.readFileSync('./test/fixtures/externalValidatorId.yaml', 'utf8')); 44 | 45 | const valid = validateConfig(config.functions.hello.events[0].http); 46 | expect(valid).toBeTruthy(); 47 | }) 48 | }); 49 | --------------------------------------------------------------------------------