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

feat: add request validator schema for http events #573

Merged
merged 2 commits into from
Sep 13, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Serverless Framework v2.32.0 or later is required.
- [Customizing response headers and templates](#customizing-response-headers-and-templates)
- [Send request to an API](#send-request-to-an-api)
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
- [Request Schema Validators](#request-schema-validators)
- [Schedule](#schedule)
- [Enabling / Disabling](#enabling--disabling)
- [Specify Name and Description](#specify-name-and-description)
Expand Down Expand Up @@ -933,6 +934,88 @@ Please note that those are the API keys names, not the actual values. Once you d

Clients connecting to this Rest API will then need to set any of these API keys values in the x-api-key header of their request. This is only necessary for functions where the private property is set to true.

#### Request Schema Validators

To use [request schema validation](https://serverless.com/framework/docs/providers/aws/events/apigateway/#request-schema-validators) 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.

```yaml
stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json: ${file(create_request.json)}
```

In addition, you can also customize created model with name and description properties.

```yaml
stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json:
schema: ${file(create_request.json)}
name: PostCreateModel
description: 'Validation model for Creating Posts'
```

To reuse the same model across different events, you can define global models on provider level. In order to define global model you need to add its configuration to `provider.apiGateway.request.schemas`. After defining a global model, you can use it in the event by referencing it by the key. Provider models are created for application/json content type.

```yaml
provider:
...
apiGateway:
request:
schemas:
post-create-model:
name: PostCreateModel
schema: ${file(api_schema/post_add_schema.json)}
description: "A Model validation for adding posts"

stepFunctions:
stateMachines:
create:
events:
- http:
path: posts/create
method: post
request:
schemas:
application/json: post-create-model
```

A sample schema contained in `create_request.json` might look something like this:

```json
{
"definitions": {},
"$schema": "http://json-schema.org/draft-04/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. Currently, API Gateway [supports](https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html) JSON Schema draft-04.

### Schedule

The following config will attach a schedule event and causes the stateMachine `crawl` to be called every 2 hours. The configuration allows you to attach multiple schedules to the same stateMachine. You can either use the `rate` or `cron` syntax. Take a look at the [AWS schedule syntax documentation](http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html) for more details.
Expand Down
160 changes: 160 additions & 0 deletions lib/deploy/events/apiGateway/requestValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
'use strict';

const BbPromise = require('bluebird');
const _ = require('lodash');

module.exports = {
compileRequestValidators() {
const apiGatewayConfig = this.serverless.service.provider.apiGateway || {};

this.pluginhttpValidated.events.forEach((event) => {
const resourceName = this.getResourceName(event.http.path);
const methodLogicalId = this.provider.naming
.getMethodLogicalId(resourceName, event.http.method);
const template = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources[methodLogicalId];
let validatorLogicalId;

if (event.http.request && event.http.request.schemas) {
for (const [contentType, schemaConfig] of Object.entries(event.http.request.schemas)) {
let modelLogicalId;

const referencedDefinitionFromProvider = !_.isObject(schemaConfig) && _.get(apiGatewayConfig, `request.schemas.${schemaConfig}`);

if (referencedDefinitionFromProvider) {
modelLogicalId = this.createProviderModel(
schemaConfig,
apiGatewayConfig.request.schemas[schemaConfig],
);
} else {
// In this situation, we have two options - schema is defined as
// string that does not reference model from provider or as an object
let modelName;
let description;
let definition;

if (_.isObject(schemaConfig)) {
if (schemaConfig.schema) {
// In this case, schema is defined as an object with explicit properties
modelName = schemaConfig.name;
description = schemaConfig.description;
definition = schemaConfig.schema;
} else {
// In this case, schema is defined as an implicit object that
// stores whole schema definition
definition = schemaConfig;
}
} else {
// In this case, schema is defined as an implicit string
definition = schemaConfig;
}

modelLogicalId = this.provider.naming.getEndpointModelLogicalId(
resourceName,
event.http.method,
contentType,
);

Object.assign(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
{
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ContentType: contentType,
Schema: definition,
Name: modelName,
Description: description,
},
},
},
);
}

if (!validatorLogicalId) {
const requestValidator = this.createRequestValidator();
validatorLogicalId = requestValidator.validatorLogicalId;
}

template.Properties.RequestValidatorId = { Ref: validatorLogicalId };
template.Properties.RequestModels = template.Properties.RequestModels || {};
template.Properties.RequestModels[contentType] = { Ref: modelLogicalId };
}
}
});

return BbPromise.resolve();
},

createProviderModel(schemaId, schemaConfig) {
let modelName;
let description;
let definition;

// If schema is not defined this will try to map resourceDefinition as the schema
if (!schemaConfig.schema) {
definition = schemaConfig;
} else {
definition = schemaConfig.schema;
}

const modelLogicalId = this.provider.naming.getModelLogicalId(schemaId);

if (schemaConfig.name) {
modelName = schemaConfig.name;
}

if (schemaConfig.description) {
description = schemaConfig.description;
}

Object.assign(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
[modelLogicalId]: {
Type: 'AWS::ApiGateway::Model',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
Schema: definition,
ContentType: 'application/json',
},
},
});

if (modelName) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
modelLogicalId
].Properties.Name = modelName;
}

if (description) {
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[
modelLogicalId
].Properties.Description = description;
}

return modelLogicalId;
},

createRequestValidator() {
const validatorLogicalId = this.provider.naming.getValidatorLogicalId();
const validatorName = `${
this.serverless.service.service
}-${this.provider.getStage()} | Validate request body and querystring parameters`;

this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[validatorLogicalId] = {
Type: 'AWS::ApiGateway::RequestValidator',
Properties: {
RestApiId: this.provider.getApiGatewayRestApiId(),
ValidateRequestBody: true,
ValidateRequestParameters: true,
Name: validatorName,
},
};

return {
validatorLogicalId,
validatorName,
};
},
};
91 changes: 91 additions & 0 deletions lib/deploy/events/apiGateway/requestValidators.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const expect = require('chai').expect;
const Serverless = require('serverless/lib/Serverless');
const AwsProvider = require('serverless/lib/plugins/aws/provider');
const ServerlessStepFunctions = require('../../../index');

describe('#requestValidator()', () => {
let serverless;
let serverlessStepFunctions;

beforeEach(() => {
const options = {
stage: 'dev',
region: 'us-east-1',
};

serverless = new Serverless();
serverless.service.service = 'step-functions';
serverless.setProvider('aws', new AwsProvider(serverless));
serverless.service.provider.compiledCloudFormationTemplate = {
Resources: {
ApiGatewayMethodFirstPost: {
Properties: {},
},
},
};
serverless.configSchemaHandler = {
// eslint-disable-next-line no-unused-vars
defineTopLevelProperty: (propertyName, propertySchema) => {},
};

serverlessStepFunctions = new ServerlessStepFunctions(serverless, options);
serverlessStepFunctions.apiGatewayRestApiLogicalId = 'ApiGatewayRestApi';
serverlessStepFunctions.apiGatewayResourceNames = {
'foo/bar1': 'apiGatewayResourceNamesFirst',
'foo/bar2': 'apiGatewayResourceNamesSecond',
};
serverlessStepFunctions.pluginhttpValidated = {
events: [
{
stateMachineName: 'first',
http: {
path: 'foo/bar1',
method: 'post',
request: {
schemas: {
'application/json': {
name: 'StartExecutionSchema',
schema: {},
},
},
},
},
},
{
stateMachineName: 'second',
http: {
path: 'foo/bar2',
method: 'post',
private: true,
},
},
],
};
serverlessStepFunctions.apiGatewayResources = {
'foo/bar1': {
name: 'First',
resourceLogicalId: 'ApiGatewayResourceFirst',
},

'foo/bar2': {
name: 'Second',
resourceLogicalId: 'ApiGatewayResourceSecond',
},
};
});

describe('#compileRequestValidators()', () => {
it('should process schema from http event request schemas', () => serverlessStepFunctions
.compileRequestValidators().then(() => {
expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources)
.to.have.property('ApiGatewayMethodFirstPostApplicationJsonModel');

expect(serverlessStepFunctions.serverless.service.provider.compiledCloudFormationTemplate
.Resources)
.to.have.property('ApiGatewayStepfunctionsRequestValidator');
}));
});
});
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const compileNotifications = require('./deploy/stepFunctions/compileNotification
const httpValidate = require('./deploy/events/apiGateway/validate');
const httpResources = require('./deploy/events/apiGateway/resources');
const httpMethods = require('./deploy/events/apiGateway/methods');
const httpRequestValidators = require('./deploy/events/apiGateway/requestValidators');

// eslint-disable-next-line max-len
const httpCors = require('./deploy/events/apiGateway/cors');
Expand Down Expand Up @@ -55,6 +56,7 @@ class ServerlessStepFunctions {
httpValidate,
httpResources,
httpMethods,
httpRequestValidators,
httpAuthorizers,
httpLambdaPermissions,
httpCors,
Expand Down Expand Up @@ -138,6 +140,7 @@ class ServerlessStepFunctions {
.then(this.compileRestApi)
.then(this.compileResources)
.then(this.compileMethods)
.then(this.compileRequestValidators)
.then(this.compileAuthorizers)
.then(this.compileHttpLambdaPermissions)
.then(this.compileCors)
Expand Down
Loading
Loading