serverless-step-functions
Version:
The module is AWS Step Functions plugin for Serverless Framework
303 lines (266 loc) • 9.66 kB
JavaScript
;
const NOT_FOUND = -1;
const _ = require('lodash');
const awsArnRegExs = require('../../../utils/arnRegularExpressions');
module.exports = {
httpValidate() {
const events = [];
const corsPreflight = {};
_.forEach(this.getAllStateMachines(), (stateMachineName) => {
const stateMachineObj = this.getStateMachine(stateMachineName);
_.forEach(stateMachineObj.events, (event) => {
if (_.has(event, 'http')) {
const http = this.getHttp(event, stateMachineName);
http.path = this.getHttpPath(http, stateMachineName);
http.method = this.getHttpMethod(http, stateMachineName);
if (http.authorizer) {
http.authorizer = this.getAuthorizer(http, stateMachineName);
}
if (http.cors) {
http.cors = this.getCors(http);
const cors = corsPreflight[http.path] || {};
cors.headers = _.union(http.cors.headers, cors.headers);
cors.methods = _.union(http.cors.methods, cors.methods);
cors.origins = _.union(http.cors.origins, cors.origins);
cors.origin = http.cors.origin || '*';
cors.allowCredentials = cors.allowCredentials || http.cors.allowCredentials;
// when merging, last one defined wins
if (_.has(http.cors, 'maxAge')) {
cors.maxAge = http.cors.maxAge;
}
corsPreflight[http.path] = cors;
}
events.push({
stateMachineName,
http,
});
}
});
});
return {
events,
corsPreflight,
};
},
getHttp(event, stateMachineName) {
if (typeof event.http === 'object') {
return event.http;
} if (typeof event.http === 'string') {
return {
method: event.http.split(' ')[0],
path: event.http.split(' ')[1],
};
}
const errorMessage = [
`Invalid http event in stateMachine "${stateMachineName}"`,
' in serverless.yml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
},
getHttpPath(http, stateMachineName) {
if (typeof http.path === 'string') {
return http.path.replace(/^\//, '').replace(/\/$/, '');
}
const errorMessage = [
`Missing or invalid "path" property in stateMachine "${stateMachineName}"`,
' for http event in serverless.yml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
},
getHttpMethod(http, stateMachineName) {
if (typeof http.method === 'string') {
const method = http.method.toLowerCase();
const allowedMethods = [
'get', 'post', 'put', 'patch', 'options', 'head', 'delete', 'any',
];
if (allowedMethods.indexOf(method) === -1) {
const errorMessage = [
`Invalid APIG method "${http.method}" in stateMachine "${stateMachineName}".`,
` AWS supported methods are: ${allowedMethods.join(', ')}.`,
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
return method;
}
const errorMessage = [
`Missing or invalid "method" property in stateMachine "${stateMachineName}"`,
' for http event in serverless.yml.',
' If you define an http event, make sure you pass a valid value for it,',
' either as string syntax, or object syntax.',
' Please check the docs for more options.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
},
getIntegration(http, stateMachineName) {
if (http.integration) {
// normalize the integration for further processing
const normalizedIntegration = http.integration.toUpperCase().replace('-', '_');
const allowedIntegrations = [
'LAMBDA_PROXY', 'LAMBDA', 'AWS', 'AWS_PROXY', 'HTTP', 'HTTP_PROXY', 'MOCK',
];
// check if the user has entered a non-valid integration
if (allowedIntegrations.indexOf(normalizedIntegration) === NOT_FOUND) {
const errorMessage = [
`Invalid APIG integration "${http.integration}"`,
` in function "${stateMachineName}".`,
' Supported integrations are:',
' lambda, lambda-proxy, aws, aws-proxy, http, http-proxy, mock.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (normalizedIntegration === 'LAMBDA') {
return 'AWS';
} if (normalizedIntegration === 'LAMBDA_PROXY') {
return 'AWS_PROXY';
}
return normalizedIntegration;
}
return 'AWS_PROXY';
},
getAuthorizer(http, functionName) {
const authorizer = http.authorizer;
let type;
let name;
let arn;
let identitySource;
let resultTtlInSeconds;
let identityValidationExpression;
let claims;
let authorizerId;
let scopes;
if (typeof authorizer === 'string') {
if (authorizer.toUpperCase() === 'AWS_IAM') {
type = 'AWS_IAM';
} else if (authorizer.indexOf(':') === -1) {
name = authorizer;
arn = this.getLambdaArn(authorizer);
} else {
arn = authorizer;
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
}
} else if (typeof authorizer === 'object') {
if (authorizer.type && authorizer.authorizerId) {
type = authorizer.type;
authorizerId = authorizer.authorizerId;
} else if (authorizer.type && authorizer.type.toUpperCase() === 'AWS_IAM') {
type = 'AWS_IAM';
} else if (authorizer.arn) {
arn = authorizer.arn;
if (_.isString(authorizer.name)) {
name = authorizer.name;
} else {
name = this.provider.naming.extractAuthorizerNameFromArn(arn);
}
} else if (authorizer.name) {
name = authorizer.name;
arn = this.getLambdaArn(name);
} else {
throw new this.serverless.classes.Error('Please provide either an authorizer name or ARN');
}
if (!type) {
type = authorizer.type;
}
if (Array.isArray(authorizer.scopes)) {
scopes = authorizer.scopes;
}
resultTtlInSeconds = Number.parseInt(authorizer.resultTtlInSeconds, 10);
resultTtlInSeconds = Number.isNaN(resultTtlInSeconds) ? 300 : resultTtlInSeconds;
claims = authorizer.claims || [];
identitySource = authorizer.identitySource;
identityValidationExpression = authorizer.identityValidationExpression;
} else {
const errorMessage = [
`authorizer property in function ${functionName} is not an object nor a string.`,
' The correct format is: authorizer: functionName',
' OR an object containing a name property.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (typeof identitySource === 'undefined') {
identitySource = 'method.request.header.Authorization';
}
const integration = this.getIntegration(http);
if (integration === 'AWS_PROXY'
&& typeof arn === 'string'
&& awsArnRegExs.cognitoIdpArnExpr.test(arn)
&& authorizer.claims) {
const errorMessage = [
'Cognito claims can only be filtered when using the lambda integration type',
];
throw new this.serverless.classes.Error(errorMessage);
}
return {
type,
name,
arn,
authorizerId,
resultTtlInSeconds,
identitySource,
identityValidationExpression,
claims,
scopes,
};
},
getCors(http) {
const headers = [
'Content-Type',
'X-Amz-Date',
'Authorization',
'X-Api-Key',
'X-Amz-Security-Token',
'X-Amz-User-Agent',
];
let cors = {
origins: ['*'],
origin: '*',
methods: ['OPTIONS'],
headers,
allowCredentials: false,
};
if (typeof http.cors === 'object') {
cors = http.cors;
cors.methods = cors.methods || [];
cors.allowCredentials = Boolean(cors.allowCredentials);
if (cors.origins && cors.origin) {
const errorMessage = [
'You can only use "origin" or "origins",',
' but not both at the same time to configure CORS.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
if (cors.headers) {
if (!Array.isArray(cors.headers)) {
const errorMessage = [
'CORS header values must be provided as an array.',
' Please check the docs for more info.',
].join('');
throw new this.serverless.classes.Error(errorMessage);
}
} else {
cors.headers = headers;
}
if (cors.methods.indexOf('OPTIONS') === NOT_FOUND) {
cors.methods.push('OPTIONS');
}
if (cors.methods.indexOf(http.method.toUpperCase()) === NOT_FOUND) {
cors.methods.push(http.method.toUpperCase());
}
} else {
cors.methods.push(http.method.toUpperCase());
}
return cors;
},
getLambdaArn(name) {
this.serverless.service.getFunction(name);
const lambdaLogicalId = this.provider.naming.getLambdaLogicalId(name);
return { 'Fn::GetAtt': [lambdaLogicalId, 'Arn'] };
},
};