Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS API Gateway request body validation #5956

Merged
merged 9 commits into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/providers/aws/events/apigateway.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ layout: Doc
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api) - [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
- [Configuring endpoint types](#configuring-endpoint-types) - [Configuring endpoint types](#configuring-endpoint-types)
- [Request Parameters](#request-parameters) - [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) - [Setting source of API key for metering requests](#setting-source-of-api-key-for-metering-requests)
- [Lambda Integration](#lambda-integration) - [Lambda Integration](#lambda-integration)
- [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization) - [Example "LAMBDA" event (before customization)](#example-lambda-event-before-customization)
Expand Down Expand Up @@ -641,6 +642,50 @@ functions:
id: true 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 ### 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: 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:
Expand Down
7 changes: 7 additions & 0 deletions lib/plugins/aws/lib/naming.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ module.exports = {
getMethodLogicalId(resourceId, methodName) { getMethodLogicalId(resourceId, methodName) {
return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`; return `ApiGatewayMethod${resourceId}${this.normalizeMethodName(methodName)}`;
}, },
getValidatorLogicalId(resourceId, methodName) {
dschep marked this conversation as resolved.
Show resolved Hide resolved
return `${this.getMethodLogicalId(resourceId, methodName)}Validator`;
},
getModelLogicalId(resourceId, methodName, contentType) {
return `${this.getMethodLogicalId(resourceId, methodName)}${_.startCase(
contentType).replace(' ', '')}Model`;
},
getApiKeyLogicalId(apiKeyNumber, apiKeyName) { getApiKeyLogicalId(apiKeyNumber, apiKeyName) {
if (apiKeyName) { if (apiKeyName) {
return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`; return `ApiGatewayApiKey${this.normalizeName(apiKeyName)}${apiKeyNumber}`;
Expand Down
16 changes: 16 additions & 0 deletions lib/plugins/aws/lib/naming.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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)', () => { describe('#getApiKeyLogicalId(keyIndex)', () => {
it('should produce the given index with ApiGatewayApiKey as a prefix', () => { it('should produce the given index with ApiGatewayApiKey as a prefix', () => {
expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1'); expect(sdk.naming.getApiKeyLogicalId(1)).to.equal('ApiGatewayApiKey1');
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ module.exports = {


const methodLogicalId = this.provider.naming const methodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, event.http.method); .getMethodLogicalId(resourceName, event.http.method);
const validatorLogicalId = this.provider.naming
.getValidatorLogicalId(resourceName, event.http.method);
const lambdaLogicalId = this.provider.naming const lambdaLogicalId = this.provider.naming
.getLambdaLogicalId(event.functionName); .getLambdaLogicalId(event.functionName);


Expand Down Expand Up @@ -67,6 +69,42 @@ module.exports = {


this.apiGatewayMethodLogicalIds.push(methodLogicalId); 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, { _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[methodLogicalId]: template, [methodLogicalId]: template,
}); });
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -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', () => { it('should have request parameters defined when they are set', () => {
awsCompileApigEvents.validated.events = [ awsCompileApigEvents.validated.events = [
{ {
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ module.exports = {
if (http.request) { if (http.request) {
const keys = Object.keys(http.request); const keys = Object.keys(http.request);
const allowedKeys = const allowedKeys =
http.integration === 'AWS_PROXY' ? ['parameters'] : ['parameters', 'uri']; http.integration === 'AWS_PROXY'
? ['parameters', 'schema']
: ['parameters', 'uri', 'schema'];


if (!_.isEmpty(_.difference(keys, allowedKeys))) { if (!_.isEmpty(_.difference(keys, allowedKeys))) {
const requestWarningMessage = [ const requestWarningMessage = [
Expand Down