AWS API Gateway request body validation (#5956)

AWS API Gateway request body validation
This commit is contained in:
Daniel Schep 2019-04-23 11:14:40 -04:00 committed by GitHub
commit b042ef02e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 1 deletions

View File

@ -24,6 +24,7 @@ layout: Doc
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
- [Configuring endpoint types](#configuring-endpoint-types)
- [Request Parameters](#request-parameters)
- [Request Schema Validation](#request-schema-validation)
- [Setting source of API key for metering requests](#setting-source-of-api-key-for-metering-requests)
- [Lambda Integration](#lambda-integration)
- [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization)
@ -649,6 +650,50 @@ functions:
id: true
```
### Request Schema Validators
To use request schema validation with API gateway, add the [JSON Schema](https://json-schema.org/)
for your content type. Since JSON Schema is represented in JSON, it's easier to include it from a
file.
```yml
functions:
create:
handler: posts.create
events:
- http:
path: posts/create
method: post
request:
schema:
application/json: ${file(create_request.json)}
```
A sample schema contained in `create_request.json` might look something like this:
```json
{
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "The Root Schema",
"required": [
"username"
],
"properties": {
"username": {
"type": "string",
"title": "The Foo Schema",
"default": "",
"pattern": "^[a-zA-Z0-9]+$"
}
}
}
```
**NOTE:** schema validators are only applied to content types you specify. Other content types are
not blocked.
### Setting source of API key for metering requests
API Gateway provide a feature for metering your API's requests and you can choice [the source of key](https://docs.aws.amazon.com/apigateway/api-reference/resource/rest-api/#apiKeySource) which is used for metering. If you want to acquire that key from the request's X-API-Key header, set option like this:

View File

@ -242,6 +242,13 @@ module.exports = {
getMethodLogicalId(resourceId, methodName) {
return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`;
},
getValidatorLogicalId(resourceId, methodName) {
return `${this.getMethodLogicalId(resourceId, methodName)}Validator`;
},
getModelLogicalId(resourceId, methodName, contentType) {
return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase(
contentType).replace(' ', '')}Model`;
},
getApiKeyLogicalId(apiKeyNumber, apiKeyName) {
if (apiKeyName) {
return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`;

View File

@ -390,6 +390,22 @@ describe('#naming()', () => {
});
});
describe('#getValidatorLogicalId()', () => {
it('', () => {
expect(sdk.naming.getValidatorLogicalId(
'ResourceId', 'get'
)).to.equal('ApiGatewayMethodResourceIdGetValidator');
});
});
describe('#getModelLogicalId()', () => {
it('', () => {
expect(sdk.naming.getModelLogicalId(
'ResourceId', 'get', 'application/json'
)).to.equal('ApiGatewayMethodResourceIdGetApplicationJsonModel');
});
});
describe('#getApiKeyLogicalId(keyIndex)', () => {
it('should produce the given index with ApiGatewayApiKey as a prefix', () => {
expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1');

View File

@ -30,6 +30,8 @@ module.exports = {
const methodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, event.http.method);
const validatorLogicalId = this.provider.naming
.getValidatorLogicalId(resourceName, event.http.method);
const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.functionName);
@ -67,6 +69,42 @@ module.exports = {
this.apiGatewayMethodLogicalIds.push(methodLogicalId);
if (event.http.request && event.http.request.schema) {
for (const requestSchema of _.entries(event.http.request.schema)) {
const contentType = requestSchema[0];
const schema = requestSchema[1];
const modelLogicalId = this.provider.naming
.getModelLogicalId(resourceName, event.http.method, contentType);
template.Properties.RequestValidatorId = { Ref: validatorLogicalId };
template.Properties.RequestModels = { [contentType]: { Ref: modelLogicalId } };
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: {
Ref: this.provider.naming.getRestApiLogicalId(),
},
ContentType: contentType,
Schema: schema,
},
},
[validatorLogicalId]: {
Type: 'AWS::ApiGateway::RequestValidator',
Properties: {
RestApiId: {
Ref: this.provider.naming.getRestApiLogicalId(),
},
ValidateRequestBody: true,
ValidateRequestParameters: true,
},
},
});
}
}
_.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[methodLogicalId]: template,
});

View File

@ -57,6 +57,54 @@ describe('#compileMethods()', () => {
};
});
it('should have request validators/models defined when they are set', () => {
awsCompileApigEvents.validated.events = [
{
functionName: 'First',
http: {
path: 'users/create',
method: 'post',
integration: 'AWS',
request: { schema: { 'application/json': { foo: 'bar' } } },
},
},
];
return awsCompileApigEvents.compileMethods().then(() => {
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePostValidator
).to.deep.equal({
Type: 'AWS::ApiGateway::RequestValidator',
Properties: {
RestApiId: { Ref: 'ApiGatewayRestApi' },
ValidateRequestBody: true,
ValidateRequestParameters: true,
},
});
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePostApplicationJsonModel
).to.deep.equal({
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: { Ref: 'ApiGatewayRestApi' },
ContentType: 'application/json',
Schema: { foo: 'bar' },
},
});
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestModels
).to.deep.equal({
'application/json': { Ref: 'ApiGatewayMethodUsersCreatePostApplicationJsonModel' },
});
expect(
awsCompileApigEvents.serverless.service.provider.compiledCloudFormationTemplate
.Resources.ApiGatewayMethodUsersCreatePost.Properties.RequestValidatorId
).to.deep.equal({ Ref: 'ApiGatewayMethodUsersCreatePostValidator' });
});
});
it('should have request parameters defined when they are set', () => {
awsCompileApigEvents.validated.events = [
{

View File

@ -98,7 +98,9 @@ module.exports = {
if (http.request) {
const keys = Object.keys(http.request);
const allowedKeys =
http.integration === 'AWS_PROXY' ? ['parameters'] : ['parameters', 'uri'];
http.integration === 'AWS_PROXY'
? ['parameters', 'schema']
: ['parameters', 'uri', 'schema'];
if (!_.isEmpty(_.difference(keys, allowedKeys))) {
const requestWarningMessage = [