Skip to content

Commit cb31547

Browse files
authoredNov 25, 2021
feat(apigateway): step functions integration (#16827)
- Added StepFunctionsRestApi and StepFunctionsIntegration implementation closes #15081. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 37596e6 commit cb31547

17 files changed

+1800
-0
lines changed
 

‎packages/@aws-cdk/aws-apigateway/README.md

+116
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ running on AWS Lambda, or any web application.
2222
- [Defining APIs](#defining-apis)
2323
- [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks)
2424
- [AWS Lambda-backed APIs](#aws-lambda-backed-apis)
25+
- [AWS StepFunctions backed APIs](#aws-stepfunctions-backed-APIs)
2526
- [Integration Targets](#integration-targets)
2627
- [Usage Plan & API Keys](#usage-plan--api-keys)
2728
- [Working with models](#working-with-models)
@@ -106,6 +107,121 @@ item.addMethod('GET'); // GET /items/{item}
106107
item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com'));
107108
```
108109

110+
## AWS StepFunctions backed APIs
111+
112+
You can use Amazon API Gateway with AWS Step Functions as the backend integration, specifically Synchronous Express Workflows.
113+
114+
The `StepFunctionsRestApi` only supports integration with Synchronous Express state machine. The `StepFunctionsRestApi` construct makes this easy by setting up input, output and error mapping.
115+
116+
The construct sets up an API endpoint and maps the `ANY` HTTP method and any calls to the API endpoint starts an express workflow execution for the underlying state machine.
117+
118+
Invoking the endpoint with any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, ...) in the example below will send the request to the state machine as a new execution. On success, an HTTP code `200` is returned with the execution output as the Response Body.
119+
120+
If the execution fails, an HTTP `500` response is returned with the `error` and `cause` from the execution output as the Response Body. If the request is invalid (ex. bad execution input) HTTP code `400` is returned.
121+
122+
The response from the invocation contains only the `output` field from the
123+
[StartSyncExecution](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartSyncExecution.html#API_StartSyncExecution_ResponseSyntax) API.
124+
In case of failures, the fields `error` and `cause` are returned as part of the response.
125+
Other metadata such as billing details, AWS account ID and resource ARNs are not returned in the API response.
126+
127+
By default, a `prod` stage is provisioned.
128+
129+
In order to reduce the payload size sent to AWS Step Functions, `headers` are not forwarded to the Step Functions execution input. It is possible to choose whether `headers`, `requestContext`, `path` and `querystring` are included or not. By default, `headers` are excluded in all requests.
130+
131+
More details about AWS Step Functions payload limit can be found at https://docs.aws.amazon.com/step-functions/latest/dg/limits-overview.html#service-limits-task-executions.
132+
133+
The following code defines a REST API that routes all requests to the specified AWS StepFunctions state machine:
134+
135+
```ts
136+
const stateMachineDefinition = new stepfunctions.Pass(this, 'PassState');
137+
138+
const stateMachine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', {
139+
definition: stateMachineDefinition,
140+
stateMachineType: stepfunctions.StateMachineType.EXPRESS,
141+
});
142+
143+
new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', {
144+
deploy: true,
145+
stateMachine: stateMachine,
146+
});
147+
```
148+
149+
When the REST API endpoint configuration above is invoked using POST, as follows -
150+
151+
```bash
152+
curl -X POST -d '{ "customerId": 1 }' https://example.com/
153+
```
154+
155+
AWS Step Functions will receive the request body in its input as follows:
156+
157+
```json
158+
{
159+
"body": {
160+
"customerId": 1
161+
},
162+
"path": "/",
163+
"querystring": {}
164+
}
165+
```
166+
167+
When the endpoint is invoked at path '/users/5' using the HTTP GET method as below:
168+
169+
```bash
170+
curl -X GET https://example.com/users/5?foo=bar
171+
```
172+
173+
AWS Step Functions will receive the following execution input:
174+
175+
```json
176+
{
177+
"body": {},
178+
"path": {
179+
"users": "5"
180+
},
181+
"querystring": {
182+
"foo": "bar"
183+
}
184+
}
185+
```
186+
187+
Additional information around the request such as the request context and headers can be included as part of the input
188+
forwarded to the state machine. The following example enables headers to be included in the input but not query string.
189+
190+
```ts fixture=stepfunctions
191+
new apigateway.StepFunctionsRestApi(this, 'StepFunctionsRestApi', {
192+
stateMachine: machine,
193+
headers: true,
194+
path: false,
195+
querystring: false,
196+
requestContext: {
197+
caller: true,
198+
user: true,
199+
},
200+
});
201+
```
202+
203+
In such a case, when the endpoint is invoked as below:
204+
205+
```bash
206+
curl -X GET https://example.com/
207+
```
208+
209+
AWS Step Functions will receive the following execution input:
210+
211+
```json
212+
{
213+
"headers": {
214+
"Accept": "...",
215+
"CloudFront-Forwarded-Proto": "...",
216+
},
217+
"requestContext": {
218+
"accountId": "...",
219+
"apiKey": "...",
220+
},
221+
"body": {}
222+
}
223+
```
224+
109225
### Breaking up Methods and Resources across Stacks
110226

111227
It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation

‎packages/@aws-cdk/aws-apigateway/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './authorizers';
2121
export * from './access-log';
2222
export * from './api-definition';
2323
export * from './gateway-response';
24+
export * from './stepfunctions-api';
2425

2526
// AWS::ApiGateway CloudFormation Resources:
2627
export * from './apigateway.generated';

‎packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ export * from './aws';
22
export * from './lambda';
33
export * from './http';
44
export * from './mock';
5+
export * from './stepfunctions';
6+
export * from './request-context';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* Configure what must be included in the `requestContext`
3+
*
4+
* More details can be found at mapping templates documentation.
5+
* @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
6+
*/
7+
export interface RequestContext {
8+
/**
9+
* Represents the information of $context.identity.accountId
10+
*
11+
* Whether the AWS account of the API owner should be included in the request context
12+
* @default false
13+
*/
14+
readonly accountId?: boolean;
15+
16+
/**
17+
* Represents the information of $context.apiId
18+
*
19+
* Whether the identifier API Gateway assigns to your API should be included in the request context.
20+
* @default false
21+
*/
22+
readonly apiId?: boolean;
23+
24+
/**
25+
* Represents the information of $context.identity.apiKey
26+
*
27+
* Whether the API key associated with the request should be included in request context.
28+
* @default false
29+
*/
30+
readonly apiKey?: boolean;
31+
32+
/**
33+
* Represents the information of $context.authorizer.principalId
34+
*
35+
* Whether the principal user identifier associated with the token sent by the client and returned
36+
* from an API Gateway Lambda authorizer should be included in the request context.
37+
* @default false
38+
*/
39+
readonly authorizerPrincipalId?: boolean;
40+
41+
/**
42+
* Represents the information of $context.identity.caller
43+
*
44+
* Whether the principal identifier of the caller that signed the request should be included in the request context.
45+
* Supported for resources that use IAM authorization.
46+
* @default false
47+
*/
48+
readonly caller?: boolean;
49+
50+
/**
51+
* Represents the information of $context.identity.cognitoAuthenticationProvider
52+
*
53+
* Whether the list of the Amazon Cognito authentication providers used by the caller making the request should be included in the request context.
54+
* Available only if the request was signed with Amazon Cognito credentials.
55+
* @default false
56+
*/
57+
readonly cognitoAuthenticationProvider?: boolean;
58+
59+
/**
60+
* Represents the information of $context.identity.cognitoAuthenticationType
61+
*
62+
* Whether the Amazon Cognito authentication type of the caller making the request should be included in the request context.
63+
* Available only if the request was signed with Amazon Cognito credentials.
64+
* Possible values include authenticated for authenticated identities and unauthenticated for unauthenticated identities.
65+
* @default false
66+
*/
67+
readonly cognitoAuthenticationType?: boolean;
68+
69+
/**
70+
* Represents the information of $context.identity.cognitoIdentityId
71+
*
72+
* Whether the Amazon Cognito identity ID of the caller making the request should be included in the request context.
73+
* Available only if the request was signed with Amazon Cognito credentials.
74+
* @default false
75+
*/
76+
readonly cognitoIdentityId?: boolean;
77+
78+
/**
79+
* Represents the information of $context.identity.cognitoIdentityPoolId
80+
*
81+
* Whether the Amazon Cognito identity pool ID of the caller making the request should be included in the request context.
82+
* Available only if the request was signed with Amazon Cognito credentials.
83+
* @default false
84+
*/
85+
readonly cognitoIdentityPoolId?: boolean;
86+
87+
/**
88+
* Represents the information of $context.httpMethod
89+
*
90+
* Whether the HTTP method used should be included in the request context.
91+
* Valid values include: DELETE, GET, HEAD, OPTIONS, PATCH, POST, and PUT.
92+
* @default false
93+
*/
94+
readonly httpMethod?: boolean;
95+
96+
/**
97+
* Represents the information of $context.stage
98+
*
99+
* Whether the deployment stage of the API request should be included in the request context.
100+
* @default false
101+
*/
102+
readonly stage?: boolean;
103+
104+
/**
105+
* Represents the information of $context.identity.sourceIp
106+
*
107+
* Whether the source IP address of the immediate TCP connection making the request
108+
* to API Gateway endpoint should be included in the request context.
109+
* @default false
110+
*/
111+
readonly sourceIp?: boolean;
112+
113+
/**
114+
* Represents the information of $context.identity.user
115+
*
116+
* Whether the principal identifier of the user that will be authorized should be included in the request context.
117+
* Supported for resources that use IAM authorization.
118+
* @default false
119+
*/
120+
readonly user?: boolean;
121+
122+
/**
123+
* Represents the information of $context.identity.userAgent
124+
*
125+
* Whether the User-Agent header of the API caller should be included in the request context.
126+
* @default false
127+
*/
128+
readonly userAgent?: boolean;
129+
130+
/**
131+
* Represents the information of $context.identity.userArn
132+
*
133+
* Whether the Amazon Resource Name (ARN) of the effective user identified after authentication should be included in the request context.
134+
* @default false
135+
*/
136+
readonly userArn?: boolean;
137+
138+
/**
139+
* Represents the information of $context.requestId
140+
*
141+
* Whether the ID for the request should be included in the request context.
142+
* @default false
143+
*/
144+
readonly requestId?: boolean;
145+
146+
/**
147+
* Represents the information of $context.resourceId
148+
*
149+
* Whether the identifier that API Gateway assigns to your resource should be included in the request context.
150+
* @default false
151+
*/
152+
readonly resourceId?: boolean;
153+
154+
/**
155+
* Represents the information of $context.resourcePath
156+
*
157+
* Whether the path to the resource should be included in the request context.
158+
* @default false
159+
*/
160+
readonly resourcePath?: boolean;
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as iam from '@aws-cdk/aws-iam';
4+
import * as sfn from '@aws-cdk/aws-stepfunctions';
5+
import { Token } from '@aws-cdk/core';
6+
import { RequestContext } from '.';
7+
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration';
8+
import { Method } from '../method';
9+
import { AwsIntegration } from './aws';
10+
/**
11+
* Options when configuring Step Functions synchronous integration with Rest API
12+
*/
13+
export interface StepFunctionsExecutionIntegrationOptions extends IntegrationOptions {
14+
15+
/**
16+
* Which details of the incoming request must be passed onto the underlying state machine,
17+
* such as, account id, user identity, request id, etc. The execution input will include a new key `requestContext`:
18+
*
19+
* {
20+
* "body": {},
21+
* "requestContext": {
22+
* "key": "value"
23+
* }
24+
* }
25+
*
26+
* @default - all parameters within request context will be set as false
27+
*/
28+
readonly requestContext?: RequestContext;
29+
30+
/**
31+
* Check if querystring is to be included inside the execution input. The execution input will include a new key `queryString`:
32+
*
33+
* {
34+
* "body": {},
35+
* "querystring": {
36+
* "key": "value"
37+
* }
38+
* }
39+
*
40+
* @default true
41+
*/
42+
readonly querystring?: boolean;
43+
44+
/**
45+
* Check if path is to be included inside the execution input. The execution input will include a new key `path`:
46+
*
47+
* {
48+
* "body": {},
49+
* "path": {
50+
* "resourceName": "resourceValue"
51+
* }
52+
* }
53+
*
54+
* @default true
55+
*/
56+
readonly path?: boolean;
57+
58+
/**
59+
* Check if header is to be included inside the execution input. The execution input will include a new key `headers`:
60+
*
61+
* {
62+
* "body": {},
63+
* "headers": {
64+
* "header1": "value",
65+
* "header2": "value"
66+
* }
67+
* }
68+
* @default false
69+
*/
70+
readonly headers?: boolean;
71+
}
72+
73+
/**
74+
* Options to integrate with various StepFunction API
75+
*/
76+
export class StepFunctionsIntegration {
77+
/**
78+
* Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method.
79+
*
80+
* @example
81+
*
82+
* const stateMachine = new stepfunctions.StateMachine(this, 'MyStateMachine', {
83+
* definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')),
84+
* });
85+
*
86+
* const api = new apigateway.RestApi(this, 'Api', {
87+
* restApiName: 'MyApi',
88+
* });
89+
* api.root.addMethod('GET', apigateway.StepFunctionsIntegration.startExecution(stateMachine));
90+
*/
91+
public static startExecution(stateMachine: sfn.IStateMachine, options?: StepFunctionsExecutionIntegrationOptions): AwsIntegration {
92+
return new StepFunctionsExecutionIntegration(stateMachine, options);
93+
}
94+
}
95+
96+
class StepFunctionsExecutionIntegration extends AwsIntegration {
97+
private readonly stateMachine: sfn.IStateMachine;
98+
constructor(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions = {}) {
99+
super({
100+
service: 'states',
101+
action: 'StartSyncExecution',
102+
options: {
103+
credentialsRole: options.credentialsRole,
104+
integrationResponses: integrationResponse(),
105+
passthroughBehavior: PassthroughBehavior.NEVER,
106+
requestTemplates: requestTemplates(stateMachine, options),
107+
...options,
108+
},
109+
});
110+
111+
this.stateMachine = stateMachine;
112+
}
113+
114+
public bind(method: Method): IntegrationConfig {
115+
const bindResult = super.bind(method);
116+
const principal = new iam.ServicePrincipal('apigateway.amazonaws.com');
117+
118+
this.stateMachine.grantExecution(principal, 'states:StartSyncExecution');
119+
120+
let stateMachineName;
121+
122+
if (this.stateMachine instanceof sfn.StateMachine) {
123+
const stateMachineType = (this.stateMachine as sfn.StateMachine).stateMachineType;
124+
if (stateMachineType !== sfn.StateMachineType.EXPRESS) {
125+
throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType');
126+
}
127+
128+
//if not imported, extract the name from the CFN layer to reach the
129+
//literal value if it is given (rather than a token)
130+
stateMachineName = (this.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineName;
131+
} else {
132+
//imported state machine
133+
stateMachineName = `StateMachine-${this.stateMachine.stack.node.addr}`;
134+
}
135+
136+
let deploymentToken;
137+
138+
if (stateMachineName !== undefined && !Token.isUnresolved(stateMachineName)) {
139+
deploymentToken = JSON.stringify({ stateMachineName });
140+
}
141+
return {
142+
...bindResult,
143+
deploymentToken,
144+
};
145+
}
146+
}
147+
148+
/**
149+
* Defines the integration response that passes the result on success,
150+
* or the error on failure, from the synchronous execution to the caller.
151+
*
152+
* @returns integrationResponse mapping
153+
*/
154+
function integrationResponse() {
155+
const errorResponse = [
156+
{
157+
/**
158+
* Specifies the regular expression (regex) pattern used to choose
159+
* an integration response based on the response from the back end.
160+
* In this case it will match all '4XX' HTTP Errors
161+
*/
162+
selectionPattern: '4\\d{2}',
163+
statusCode: '400',
164+
responseTemplates: {
165+
'application/json': `{
166+
"error": "Bad request!"
167+
}`,
168+
},
169+
},
170+
{
171+
/**
172+
* Match all '5XX' HTTP Errors
173+
*/
174+
selectionPattern: '5\\d{2}',
175+
statusCode: '500',
176+
responseTemplates: {
177+
'application/json': '"error": $input.path(\'$.error\')',
178+
},
179+
},
180+
];
181+
182+
const integResponse = [
183+
{
184+
statusCode: '200',
185+
responseTemplates: {
186+
/* eslint-disable */
187+
'application/json': [
188+
'#set($inputRoot = $input.path(\'$\'))',
189+
'#if($input.path(\'$.status\').toString().equals("FAILED"))',
190+
'#set($context.responseOverride.status = 500)',
191+
'{',
192+
'"error": "$input.path(\'$.error\')"',
193+
'"cause": "$input.path(\'$.cause\')"',
194+
'}',
195+
'#else',
196+
'$input.path(\'$.output\')',
197+
'#end',
198+
/* eslint-enable */
199+
].join('\n'),
200+
},
201+
},
202+
...errorResponse,
203+
];
204+
205+
return integResponse;
206+
}
207+
208+
/**
209+
* Defines the request template that will be used for the integration
210+
* @param stateMachine
211+
* @param options
212+
* @returns requestTemplate
213+
*/
214+
function requestTemplates(stateMachine: sfn.IStateMachine, options: StepFunctionsExecutionIntegrationOptions) {
215+
const templateStr = templateString(stateMachine, options);
216+
217+
const requestTemplate: { [contentType:string] : string } =
218+
{
219+
'application/json': templateStr,
220+
};
221+
222+
return requestTemplate;
223+
}
224+
225+
/**
226+
* Reads the VTL template and returns the template string to be used
227+
* for the request template.
228+
*
229+
* @param stateMachine
230+
* @param includeRequestContext
231+
* @param options
232+
* @reutrns templateString
233+
*/
234+
function templateString(
235+
stateMachine: sfn.IStateMachine,
236+
options: StepFunctionsExecutionIntegrationOptions): string {
237+
let templateStr: string;
238+
239+
let requestContextStr = '';
240+
241+
const includeHeader = options.headers?? false;
242+
const includeQueryString = options.querystring?? true;
243+
const includePath = options.path?? true;
244+
245+
if (options.requestContext && Object.keys(options.requestContext).length > 0) {
246+
requestContextStr = requestContext(options.requestContext);
247+
}
248+
249+
templateStr = fs.readFileSync(path.join(__dirname, 'stepfunctions.vtl'), { encoding: 'utf-8' });
250+
templateStr = templateStr.replace('%STATEMACHINE%', stateMachine.stateMachineArn);
251+
templateStr = templateStr.replace('%INCLUDE_HEADERS%', String(includeHeader));
252+
templateStr = templateStr.replace('%INCLUDE_QUERYSTRING%', String(includeQueryString));
253+
templateStr = templateStr.replace('%INCLUDE_PATH%', String(includePath));
254+
templateStr = templateStr.replace('%REQUESTCONTEXT%', requestContextStr);
255+
256+
return templateStr;
257+
}
258+
259+
function requestContext(requestContextObj: RequestContext | undefined): string {
260+
const context = {
261+
accountId: requestContextObj?.accountId? '$context.identity.accountId': undefined,
262+
apiId: requestContextObj?.apiId? '$context.apiId': undefined,
263+
apiKey: requestContextObj?.apiKey? '$context.identity.apiKey': undefined,
264+
authorizerPrincipalId: requestContextObj?.authorizerPrincipalId? '$context.authorizer.principalId': undefined,
265+
caller: requestContextObj?.caller? '$context.identity.caller': undefined,
266+
cognitoAuthenticationProvider: requestContextObj?.cognitoAuthenticationProvider? '$context.identity.cognitoAuthenticationProvider': undefined,
267+
cognitoAuthenticationType: requestContextObj?.cognitoAuthenticationType? '$context.identity.cognitoAuthenticationType': undefined,
268+
cognitoIdentityId: requestContextObj?.cognitoIdentityId? '$context.identity.cognitoIdentityId': undefined,
269+
cognitoIdentityPoolId: requestContextObj?.cognitoIdentityPoolId? '$context.identity.cognitoIdentityPoolId': undefined,
270+
httpMethod: requestContextObj?.httpMethod? '$context.httpMethod': undefined,
271+
stage: requestContextObj?.stage? '$context.stage': undefined,
272+
sourceIp: requestContextObj?.sourceIp? '$context.identity.sourceIp': undefined,
273+
user: requestContextObj?.user? '$context.identity.user': undefined,
274+
userAgent: requestContextObj?.userAgent? '$context.identity.userAgent': undefined,
275+
userArn: requestContextObj?.userArn? '$context.identity.userArn': undefined,
276+
requestId: requestContextObj?.requestId? '$context.requestId': undefined,
277+
resourceId: requestContextObj?.resourceId? '$context.resourceId': undefined,
278+
resourcePath: requestContextObj?.resourcePath? '$context.resourcePath': undefined,
279+
};
280+
281+
const contextAsString = JSON.stringify(context);
282+
283+
// The VTL Template conflicts with double-quotes (") for strings.
284+
// Before sending to the template, we replace double-quotes (") with @@ and replace it back inside the .vtl file
285+
const doublequotes = '"';
286+
const replaceWith = '@@';
287+
return contextAsString.split(doublequotes).join(replaceWith);
288+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Velocity Template used for API Gateway request mapping template
2+
##
3+
## This template forwards the request body, header, path, and querystring
4+
## to the execution input of the state machine.
5+
##
6+
## "@@" is used here as a placeholder for '"' to avoid using escape characters.
7+
8+
#set($inputString = '')
9+
#set($includeHeaders = %INCLUDE_HEADERS%)
10+
#set($includeQueryString = %INCLUDE_QUERYSTRING%)
11+
#set($includePath = %INCLUDE_PATH%)
12+
#set($allParams = $input.params())
13+
{
14+
"stateMachineArn": "%STATEMACHINE%",
15+
16+
#set($inputString = "$inputString,@@body@@: $input.body")
17+
18+
#if ($includeHeaders)
19+
#set($inputString = "$inputString, @@header@@:{")
20+
#foreach($paramName in $allParams.header.keySet())
21+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@")
22+
#if($foreach.hasNext)
23+
#set($inputString = "$inputString,")
24+
#end
25+
#end
26+
#set($inputString = "$inputString }")
27+
28+
#end
29+
30+
#if ($includeQueryString)
31+
#set($inputString = "$inputString, @@querystring@@:{")
32+
#foreach($paramName in $allParams.querystring.keySet())
33+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@")
34+
#if($foreach.hasNext)
35+
#set($inputString = "$inputString,")
36+
#end
37+
#end
38+
#set($inputString = "$inputString }")
39+
#end
40+
41+
#if ($includePath)
42+
#set($inputString = "$inputString, @@path@@:{")
43+
#foreach($paramName in $allParams.path.keySet())
44+
#set($inputString = "$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@")
45+
#if($foreach.hasNext)
46+
#set($inputString = "$inputString,")
47+
#end
48+
#end
49+
#set($inputString = "$inputString }")
50+
#end
51+
52+
#set($requestContext = "%REQUESTCONTEXT%")
53+
## Check if the request context should be included as part of the execution input
54+
#if($requestContext && !$requestContext.empty)
55+
#set($inputString = "$inputString,")
56+
#set($inputString = "$inputString @@requestContext@@: $requestContext")
57+
#end
58+
59+
#set($inputString = "$inputString}")
60+
#set($inputString = $inputString.replaceAll("@@",'"'))
61+
#set($len = $inputString.length() - 1)
62+
"input": "{$util.escapeJavaScript($inputString.substring(1,$len))}"
63+
}

‎packages/@aws-cdk/aws-apigateway/lib/restapi.ts

+1
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export abstract class RestApiBase extends Resource implements IRestApi {
312312

313313
/**
314314
* A human friendly name for this Rest API. Note that this is different from `restApiId`.
315+
* @attribute
315316
*/
316317
public readonly restApiName: string;
317318

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Construct } from 'constructs';
4+
import { RestApi, RestApiProps } from '.';
5+
import { RequestContext } from './integrations';
6+
import { StepFunctionsIntegration } from './integrations/stepfunctions';
7+
import { Model } from './model';
8+
9+
/**
10+
* Properties for StepFunctionsRestApi
11+
*
12+
*/
13+
export interface StepFunctionsRestApiProps extends RestApiProps {
14+
/**
15+
* The default State Machine that handles all requests from this API.
16+
*
17+
* This stateMachine will be used as a the default integration for all methods in
18+
* this API, unless specified otherwise in `addMethod`.
19+
*/
20+
readonly stateMachine: sfn.IStateMachine;
21+
22+
/**
23+
* Which details of the incoming request must be passed onto the underlying state machine,
24+
* such as, account id, user identity, request id, etc. The execution input will include a new key `requestContext`:
25+
*
26+
* {
27+
* "body": {},
28+
* "requestContext": {
29+
* "key": "value"
30+
* }
31+
* }
32+
*
33+
* @default - all parameters within request context will be set as false
34+
*/
35+
readonly requestContext?: RequestContext;
36+
37+
/**
38+
* Check if querystring is to be included inside the execution input. The execution input will include a new key `queryString`:
39+
*
40+
* {
41+
* "body": {},
42+
* "querystring": {
43+
* "key": "value"
44+
* }
45+
* }
46+
*
47+
* @default true
48+
*/
49+
readonly querystring?: boolean;
50+
51+
/**
52+
* Check if path is to be included inside the execution input. The execution input will include a new key `path`:
53+
*
54+
* {
55+
* "body": {},
56+
* "path": {
57+
* "resourceName": "resourceValue"
58+
* }
59+
* }
60+
*
61+
* @default true
62+
*/
63+
readonly path?: boolean;
64+
65+
/**
66+
* Check if header is to be included inside the execution input. The execution input will include a new key `headers`:
67+
*
68+
* {
69+
* "body": {},
70+
* "headers": {
71+
* "header1": "value",
72+
* "header2": "value"
73+
* }
74+
* }
75+
* @default false
76+
*/
77+
readonly headers?: boolean;
78+
}
79+
80+
/**
81+
* Defines an API Gateway REST API with a Synchrounous Express State Machine as a proxy integration.
82+
*/
83+
export class StepFunctionsRestApi extends RestApi {
84+
constructor(scope: Construct, id: string, props: StepFunctionsRestApiProps) {
85+
if (props.defaultIntegration) {
86+
throw new Error('Cannot specify "defaultIntegration" since Step Functions integration is automatically defined');
87+
}
88+
89+
if ((props.stateMachine.node.defaultChild as sfn.CfnStateMachine).stateMachineType !== sfn.StateMachineType.EXPRESS) {
90+
throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType');
91+
}
92+
93+
const stepfunctionsIntegration = StepFunctionsIntegration.startExecution(props.stateMachine, {
94+
credentialsRole: role(scope, props),
95+
requestContext: props.requestContext,
96+
path: props.path?? true,
97+
querystring: props.querystring?? true,
98+
headers: props.headers,
99+
});
100+
101+
super(scope, id, props);
102+
103+
this.root.addMethod('ANY', stepfunctionsIntegration, {
104+
methodResponses: methodResponse(),
105+
});
106+
}
107+
}
108+
109+
/**
110+
* Defines the IAM Role for API Gateway with required permissions
111+
* to invoke a synchronous execution for the provided state machine
112+
*
113+
* @param scope
114+
* @param props
115+
* @returns Role - IAM Role
116+
*/
117+
function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role {
118+
const roleName: string = 'StartSyncExecutionRole';
119+
const apiRole = new iam.Role(scope, roleName, {
120+
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
121+
});
122+
123+
props.stateMachine.grantStartSyncExecution(apiRole);
124+
125+
return apiRole;
126+
}
127+
128+
/**
129+
* Defines the method response modelfor each HTTP code response
130+
* @returns methodResponse
131+
*/
132+
function methodResponse() {
133+
return [
134+
{
135+
statusCode: '200',
136+
responseModels: {
137+
'application/json': Model.EMPTY_MODEL,
138+
},
139+
},
140+
{
141+
statusCode: '400',
142+
responseModels: {
143+
'application/json': Model.ERROR_MODEL,
144+
},
145+
},
146+
{
147+
statusCode: '500',
148+
responseModels: {
149+
'application/json': Model.ERROR_MODEL,
150+
},
151+
},
152+
];
153+
}

‎packages/@aws-cdk/aws-apigateway/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"@aws-cdk/aws-logs": "0.0.0",
9898
"@aws-cdk/aws-s3": "0.0.0",
9999
"@aws-cdk/aws-s3-assets": "0.0.0",
100+
"@aws-cdk/aws-stepfunctions": "0.0.0",
100101
"@aws-cdk/core": "0.0.0",
101102
"@aws-cdk/cx-api": "0.0.0",
102103
"constructs": "^3.3.69"
@@ -113,6 +114,7 @@
113114
"@aws-cdk/aws-logs": "0.0.0",
114115
"@aws-cdk/aws-s3": "0.0.0",
115116
"@aws-cdk/aws-s3-assets": "0.0.0",
117+
"@aws-cdk/aws-stepfunctions": "0.0.0",
116118
"@aws-cdk/core": "0.0.0",
117119
"@aws-cdk/cx-api": "0.0.0",
118120
"constructs": "^3.3.69"

‎packages/@aws-cdk/aws-apigateway/rosetta/default.ts-fixture

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import iam = require('@aws-cdk/aws-iam');
88
import s3 = require('@aws-cdk/aws-s3');
99
import ec2 = require('@aws-cdk/aws-ec2');
1010
import logs = require('@aws-cdk/aws-logs');
11+
import stepfunctions = require('@aws-cdk/aws-stepfunctions');
1112

1213
class Fixture extends Stack {
1314
constructor(scope: Construct, id: string) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Construct } from 'constructs';
2+
import { Stack } from '@aws-cdk/core';
3+
import apigateway = require('@aws-cdk/aws-apigateway');
4+
import stepfunctions = require('@aws-cdk/aws-stepfunctions');
5+
6+
class Fixture extends Stack {
7+
constructor(scope: Construct, id: string) {
8+
super(scope, id);
9+
10+
const machine: stepfunctions.IStateMachine = new stepfunctions.StateMachine(this, 'StateMachine', {
11+
definition: new stepfunctions.Pass(this, 'PassState'),
12+
stateMachineType: stepfunctions.StateMachineType.EXPRESS,
13+
});
14+
15+
/// here
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
{
2+
"Resources": {
3+
"StateMachineRoleB840431D": {
4+
"Type": "AWS::IAM::Role",
5+
"Properties": {
6+
"AssumeRolePolicyDocument": {
7+
"Statement": [
8+
{
9+
"Action": "sts:AssumeRole",
10+
"Effect": "Allow",
11+
"Principal": {
12+
"Service": {
13+
"Fn::Join": [
14+
"",
15+
[
16+
"states.",
17+
{
18+
"Ref": "AWS::Region"
19+
},
20+
".amazonaws.com"
21+
]
22+
]
23+
}
24+
}
25+
}
26+
],
27+
"Version": "2012-10-17"
28+
}
29+
}
30+
},
31+
"StateMachine2E01A3A5": {
32+
"Type": "AWS::StepFunctions::StateMachine",
33+
"Properties": {
34+
"RoleArn": {
35+
"Fn::GetAtt": [
36+
"StateMachineRoleB840431D",
37+
"Arn"
38+
]
39+
},
40+
"DefinitionString": "{\"StartAt\":\"PassTask\",\"States\":{\"PassTask\":{\"Type\":\"Pass\",\"Result\":\"Hello\",\"End\":true}}}",
41+
"StateMachineType": "EXPRESS"
42+
},
43+
"DependsOn": [
44+
"StateMachineRoleB840431D"
45+
]
46+
},
47+
"StartSyncExecutionRoleDE73CB90": {
48+
"Type": "AWS::IAM::Role",
49+
"Properties": {
50+
"AssumeRolePolicyDocument": {
51+
"Statement": [
52+
{
53+
"Action": "sts:AssumeRole",
54+
"Effect": "Allow",
55+
"Principal": {
56+
"Service": "apigateway.amazonaws.com"
57+
}
58+
}
59+
],
60+
"Version": "2012-10-17"
61+
}
62+
}
63+
},
64+
"StartSyncExecutionRoleDefaultPolicy5A5803F8": {
65+
"Type": "AWS::IAM::Policy",
66+
"Properties": {
67+
"PolicyDocument": {
68+
"Statement": [
69+
{
70+
"Action": "states:StartSyncExecution",
71+
"Effect": "Allow",
72+
"Resource": {
73+
"Ref": "StateMachine2E01A3A5"
74+
}
75+
}
76+
],
77+
"Version": "2012-10-17"
78+
},
79+
"PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8",
80+
"Roles": [
81+
{
82+
"Ref": "StartSyncExecutionRoleDE73CB90"
83+
}
84+
]
85+
}
86+
},
87+
"StepFunctionsRestApiC6E3E883": {
88+
"Type": "AWS::ApiGateway::RestApi",
89+
"Properties": {
90+
"Name": "StepFunctionsRestApi"
91+
}
92+
},
93+
"StepFunctionsRestApiCloudWatchRoleB06ACDB9": {
94+
"Type": "AWS::IAM::Role",
95+
"Properties": {
96+
"AssumeRolePolicyDocument": {
97+
"Statement": [
98+
{
99+
"Action": "sts:AssumeRole",
100+
"Effect": "Allow",
101+
"Principal": {
102+
"Service": "apigateway.amazonaws.com"
103+
}
104+
}
105+
],
106+
"Version": "2012-10-17"
107+
},
108+
"ManagedPolicyArns": [
109+
{
110+
"Fn::Join": [
111+
"",
112+
[
113+
"arn:",
114+
{
115+
"Ref": "AWS::Partition"
116+
},
117+
":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
118+
]
119+
]
120+
}
121+
]
122+
}
123+
},
124+
"StepFunctionsRestApiAccountBD0CCC0E": {
125+
"Type": "AWS::ApiGateway::Account",
126+
"Properties": {
127+
"CloudWatchRoleArn": {
128+
"Fn::GetAtt": [
129+
"StepFunctionsRestApiCloudWatchRoleB06ACDB9",
130+
"Arn"
131+
]
132+
}
133+
},
134+
"DependsOn": [
135+
"StepFunctionsRestApiC6E3E883"
136+
]
137+
},
138+
"StepFunctionsRestApiANY7699CA92": {
139+
"Type": "AWS::ApiGateway::Method",
140+
"Properties": {
141+
"HttpMethod": "ANY",
142+
"ResourceId": {
143+
"Fn::GetAtt": [
144+
"StepFunctionsRestApiC6E3E883",
145+
"RootResourceId"
146+
]
147+
},
148+
"RestApiId": {
149+
"Ref": "StepFunctionsRestApiC6E3E883"
150+
},
151+
"AuthorizationType": "NONE",
152+
"Integration": {
153+
"Credentials": {
154+
"Fn::GetAtt": [
155+
"StartSyncExecutionRoleDE73CB90",
156+
"Arn"
157+
]
158+
},
159+
"IntegrationHttpMethod": "POST",
160+
"IntegrationResponses": [
161+
{
162+
"ResponseTemplates": {
163+
"application/json": "#set($inputRoot = $input.path('$'))\n#if($input.path('$.status').toString().equals(\"FAILED\"))\n#set($context.responseOverride.status = 500)\n{\n\"error\": \"$input.path('$.error')\"\n\"cause\": \"$input.path('$.cause')\"\n}\n#else\n$input.path('$.output')\n#end"
164+
},
165+
"StatusCode": "200"
166+
},
167+
{
168+
"ResponseTemplates": {
169+
"application/json": "{\n \"error\": \"Bad request!\"\n }"
170+
},
171+
"SelectionPattern": "4\\d{2}",
172+
"StatusCode": "400"
173+
},
174+
{
175+
"ResponseTemplates": {
176+
"application/json": "\"error\": $input.path('$.error')"
177+
},
178+
"SelectionPattern": "5\\d{2}",
179+
"StatusCode": "500"
180+
}
181+
],
182+
"PassthroughBehavior": "NEVER",
183+
"RequestTemplates": {
184+
"application/json": {
185+
"Fn::Join": [
186+
"",
187+
[
188+
"## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = true)\n#set($includeQueryString = false)\n#set($includePath = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"",
189+
{
190+
"Ref": "StateMachine2E01A3A5"
191+
},
192+
"\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"{@@accountId@@:@@$context.identity.accountId@@,@@userArn@@:@@$context.identity.userArn@@}\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n"
193+
]
194+
]
195+
}
196+
},
197+
"Type": "AWS",
198+
"Uri": {
199+
"Fn::Join": [
200+
"",
201+
[
202+
"arn:",
203+
{
204+
"Ref": "AWS::Partition"
205+
},
206+
":apigateway:",
207+
{
208+
"Ref": "AWS::Region"
209+
},
210+
":states:action/StartSyncExecution"
211+
]
212+
]
213+
}
214+
},
215+
"MethodResponses": [
216+
{
217+
"ResponseModels": {
218+
"application/json": "Empty"
219+
},
220+
"StatusCode": "200"
221+
},
222+
{
223+
"ResponseModels": {
224+
"application/json": "Error"
225+
},
226+
"StatusCode": "400"
227+
},
228+
{
229+
"ResponseModels": {
230+
"application/json": "Error"
231+
},
232+
"StatusCode": "500"
233+
}
234+
]
235+
}
236+
},
237+
"deployment33381975b5dafda9a97138f301ea25da405640e8": {
238+
"Type": "AWS::ApiGateway::Deployment",
239+
"Properties": {
240+
"RestApiId": {
241+
"Ref": "StepFunctionsRestApiC6E3E883"
242+
}
243+
},
244+
"DependsOn": [
245+
"StepFunctionsRestApiANY7699CA92"
246+
]
247+
},
248+
"stage0661E4AC": {
249+
"Type": "AWS::ApiGateway::Stage",
250+
"Properties": {
251+
"RestApiId": {
252+
"Ref": "StepFunctionsRestApiC6E3E883"
253+
},
254+
"DeploymentId": {
255+
"Ref": "deployment33381975b5dafda9a97138f301ea25da405640e8"
256+
},
257+
"StageName": "prod"
258+
}
259+
}
260+
},
261+
"Outputs": {
262+
"ApiEndpoint": {
263+
"Value": {
264+
"Fn::Join": [
265+
"",
266+
[
267+
"https://",
268+
{
269+
"Ref": "StepFunctionsRestApiC6E3E883"
270+
},
271+
".execute-api.",
272+
{
273+
"Ref": "AWS::Region"
274+
},
275+
".",
276+
{
277+
"Ref": "AWS::URLSuffix"
278+
},
279+
"/",
280+
{
281+
"Ref": "stage0661E4AC"
282+
},
283+
"/"
284+
]
285+
]
286+
}
287+
}
288+
}
289+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as sfn from '@aws-cdk/aws-stepfunctions';
2+
import * as cdk from '@aws-cdk/core';
3+
import { Construct } from 'constructs';
4+
import * as apigw from '../lib';
5+
6+
/**
7+
* Stack verification steps:
8+
* * `curl -X POST 'https://<api-id>.execute-api.<region>.amazonaws.com/prod' \
9+
* * -d '{"key":"Hello"}' -H 'Content-Type: application/json'`
10+
* The above should return a "Hello" response
11+
*/
12+
13+
class StepFunctionsRestApiDeploymentStack extends cdk.Stack {
14+
constructor(scope: Construct) {
15+
super(scope, 'StepFunctionsRestApiDeploymentStack');
16+
17+
const passTask = new sfn.Pass(this, 'PassTask', {
18+
result: { value: 'Hello' },
19+
});
20+
21+
const stateMachine = new sfn.StateMachine(this, 'StateMachine', {
22+
definition: passTask,
23+
stateMachineType: sfn.StateMachineType.EXPRESS,
24+
});
25+
26+
const api = new apigw.StepFunctionsRestApi(this, 'StepFunctionsRestApi', {
27+
deploy: false,
28+
stateMachine: stateMachine,
29+
headers: true,
30+
path: false,
31+
querystring: false,
32+
requestContext: {
33+
accountId: true,
34+
userArn: true,
35+
},
36+
});
37+
38+
api.deploymentStage = new apigw.Stage(this, 'stage', {
39+
deployment: new apigw.Deployment(this, 'deployment', {
40+
api,
41+
}),
42+
});
43+
44+
new cdk.CfnOutput(this, 'ApiEndpoint', {
45+
value: api.url,
46+
});
47+
}
48+
}
49+
50+
const app = new cdk.App();
51+
new StepFunctionsRestApiDeploymentStack(app);
52+
app.synth();

‎packages/@aws-cdk/aws-apigateway/test/integrations/stepfunctions.test.ts

+407
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import '@aws-cdk/assert-internal/jest';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { StateMachine } from '@aws-cdk/aws-stepfunctions';
4+
import * as cdk from '@aws-cdk/core';
5+
import * as apigw from '../lib';
6+
7+
describe('Step Functions api', () => {
8+
test('StepFunctionsRestApi defines correct REST API resources', () => {
9+
//GIVEN
10+
const { stack, stateMachine } = givenSetup();
11+
12+
//WHEN
13+
const api = whenCondition(stack, stateMachine);
14+
15+
expect(() => {
16+
api.root.addResource('not allowed');
17+
}).toThrow();
18+
19+
//THEN
20+
expect(stack).toHaveResource('AWS::ApiGateway::Method', {
21+
HttpMethod: 'ANY',
22+
MethodResponses: getMethodResponse(),
23+
AuthorizationType: 'NONE',
24+
RestApiId: {
25+
Ref: 'StepFunctionsRestApiC6E3E883',
26+
},
27+
ResourceId: {
28+
'Fn::GetAtt': [
29+
'StepFunctionsRestApiC6E3E883',
30+
'RootResourceId',
31+
],
32+
},
33+
Integration: {
34+
Credentials: {
35+
'Fn::GetAtt': [
36+
'StartSyncExecutionRoleDE73CB90',
37+
'Arn',
38+
],
39+
},
40+
IntegrationHttpMethod: 'POST',
41+
IntegrationResponses: getIntegrationResponse(),
42+
RequestTemplates: {
43+
'application/json': {
44+
'Fn::Join': [
45+
'',
46+
[
47+
"## Velocity Template used for API Gateway request mapping template\n##\n## This template forwards the request body, header, path, and querystring\n## to the execution input of the state machine.\n##\n## \"@@\" is used here as a placeholder for '\"' to avoid using escape characters.\n\n#set($inputString = '')\n#set($includeHeaders = false)\n#set($includeQueryString = true)\n#set($includePath = true)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"",
48+
{
49+
Ref: 'StateMachine2E01A3A5',
50+
},
51+
"\",\n\n #set($inputString = \"$inputString,@@body@@: $input.body\")\n\n #if ($includeHeaders)\n #set($inputString = \"$inputString, @@header@@:{\")\n #foreach($paramName in $allParams.header.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.header.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n \n #end\n\n #if ($includeQueryString)\n #set($inputString = \"$inputString, @@querystring@@:{\")\n #foreach($paramName in $allParams.querystring.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.querystring.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n\n #if ($includePath)\n #set($inputString = \"$inputString, @@path@@:{\")\n #foreach($paramName in $allParams.path.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($allParams.path.get($paramName))@@\")\n #if($foreach.hasNext)\n #set($inputString = \"$inputString,\")\n #end\n #end\n #set($inputString = \"$inputString }\")\n #end\n \n #set($requestContext = \"\")\n ## Check if the request context should be included as part of the execution input\n #if($requestContext && !$requestContext.empty)\n #set($inputString = \"$inputString,\")\n #set($inputString = \"$inputString @@requestContext@@: $requestContext\")\n #end\n\n #set($inputString = \"$inputString}\")\n #set($inputString = $inputString.replaceAll(\"@@\",'\"'))\n #set($len = $inputString.length() - 1)\n \"input\": \"{$util.escapeJavaScript($inputString.substring(1,$len))}\"\n}\n",
52+
],
53+
],
54+
},
55+
},
56+
Type: 'AWS',
57+
Uri: {
58+
'Fn::Join': [
59+
'',
60+
[
61+
'arn:',
62+
{
63+
Ref: 'AWS::Partition',
64+
},
65+
':apigateway:',
66+
{
67+
Ref: 'AWS::Region',
68+
},
69+
':states:action/StartSyncExecution',
70+
],
71+
],
72+
},
73+
PassthroughBehavior: 'NEVER',
74+
},
75+
});
76+
});
77+
78+
test('fails if options.defaultIntegration is set', () => {
79+
//GIVEN
80+
const { stack, stateMachine } = givenSetup();
81+
82+
const httpURL: string = 'https://foo/bar';
83+
84+
//WHEN & THEN
85+
expect(() => new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', {
86+
stateMachine: stateMachine,
87+
defaultIntegration: new apigw.HttpIntegration(httpURL),
88+
})).toThrow(/Cannot specify \"defaultIntegration\" since Step Functions integration is automatically defined/);
89+
90+
});
91+
92+
test('fails if State Machine is not of type EXPRESS', () => {
93+
//GIVEN
94+
const stack = new cdk.Stack();
95+
96+
const passTask = new sfn.Pass(stack, 'passTask', {
97+
inputPath: '$.somekey',
98+
});
99+
100+
const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', {
101+
definition: passTask,
102+
stateMachineType: sfn.StateMachineType.STANDARD,
103+
});
104+
105+
//WHEN & THEN
106+
expect(() => new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', {
107+
stateMachine: stateMachine,
108+
})).toThrow(/State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType/);
109+
});
110+
});
111+
112+
function givenSetup() {
113+
const stack = new cdk.Stack();
114+
115+
const passTask = new sfn.Pass(stack, 'passTask', {
116+
inputPath: '$.somekey',
117+
});
118+
119+
const stateMachine: sfn.IStateMachine = new StateMachine(stack, 'StateMachine', {
120+
definition: passTask,
121+
stateMachineType: sfn.StateMachineType.EXPRESS,
122+
});
123+
124+
return { stack, stateMachine };
125+
}
126+
127+
function whenCondition(stack:cdk.Stack, stateMachine: sfn.IStateMachine) {
128+
const api = new apigw.StepFunctionsRestApi(stack, 'StepFunctionsRestApi', { stateMachine: stateMachine });
129+
return api;
130+
}
131+
132+
function getMethodResponse() {
133+
return [
134+
{
135+
StatusCode: '200',
136+
ResponseModels: {
137+
'application/json': 'Empty',
138+
},
139+
},
140+
{
141+
StatusCode: '400',
142+
ResponseModels: {
143+
'application/json': 'Error',
144+
},
145+
},
146+
{
147+
StatusCode: '500',
148+
ResponseModels: {
149+
'application/json': 'Error',
150+
},
151+
},
152+
];
153+
}
154+
155+
function getIntegrationResponse() {
156+
const errorResponse = [
157+
{
158+
SelectionPattern: '4\\d{2}',
159+
StatusCode: '400',
160+
ResponseTemplates: {
161+
'application/json': `{
162+
"error": "Bad request!"
163+
}`,
164+
},
165+
},
166+
{
167+
SelectionPattern: '5\\d{2}',
168+
StatusCode: '500',
169+
ResponseTemplates: {
170+
'application/json': '"error": $input.path(\'$.error\')',
171+
},
172+
},
173+
];
174+
175+
const integResponse = [
176+
{
177+
StatusCode: '200',
178+
ResponseTemplates: {
179+
'application/json': [
180+
'#set($inputRoot = $input.path(\'$\'))',
181+
'#if($input.path(\'$.status\').toString().equals("FAILED"))',
182+
'#set($context.responseOverride.status = 500)',
183+
'{',
184+
'"error": "$input.path(\'$.error\')"',
185+
'"cause": "$input.path(\'$.cause\')"',
186+
'}',
187+
'#else',
188+
'$input.path(\'$.output\')',
189+
'#end',
190+
].join('\n'),
191+
},
192+
},
193+
...errorResponse,
194+
];
195+
196+
return integResponse;
197+
}

‎packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts

+20
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,18 @@ abstract class StateMachineBase extends Resource implements IStateMachine {
164164
});
165165
}
166166

167+
/**
168+
* Grant the given identity permissions to start a synchronous execution of
169+
* this state machine.
170+
*/
171+
public grantStartSyncExecution(identity: iam.IGrantable): iam.Grant {
172+
return iam.Grant.addToPrincipal({
173+
grantee: identity,
174+
actions: ['states:StartSyncExecution'],
175+
resourceArns: [this.stateMachineArn],
176+
});
177+
}
178+
167179
/**
168180
* Grant the given identity permissions to read results from state
169181
* machine.
@@ -505,6 +517,14 @@ export interface IStateMachine extends IResource, iam.IGrantable {
505517
*/
506518
grantStartExecution(identity: iam.IGrantable): iam.Grant;
507519

520+
/**
521+
* Grant the given identity permissions to start a synchronous execution of
522+
* this state machine.
523+
*
524+
* @param identity The principal
525+
*/
526+
grantStartSyncExecution(identity: iam.IGrantable): iam.Grant;
527+
508528
/**
509529
* Grant the given identity read permissions for this state machine
510530
*

‎packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,36 @@ describe('State Machine Resources', () => {
196196

197197
}),
198198

199+
test('Created state machine can grant start sync execution to a role', () => {
200+
// GIVEN
201+
const stack = new cdk.Stack();
202+
const task = new FakeTask(stack, 'Task');
203+
const stateMachine = new stepfunctions.StateMachine(stack, 'StateMachine', {
204+
definition: task,
205+
stateMachineType: stepfunctions.StateMachineType.EXPRESS,
206+
});
207+
const role = new iam.Role(stack, 'Role', {
208+
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
209+
});
210+
211+
// WHEN
212+
stateMachine.grantStartSyncExecution(role);
213+
214+
// THEN
215+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
216+
PolicyDocument: {
217+
Statement: Match.arrayWith([Match.objectLike({
218+
Action: 'states:StartSyncExecution',
219+
Effect: 'Allow',
220+
Resource: {
221+
Ref: 'StateMachine2E01A3A5',
222+
},
223+
})]),
224+
},
225+
});
226+
227+
}),
228+
199229
test('Created state machine can grant read access to a role', () => {
200230
// GIVEN
201231
const stack = new cdk.Stack();

0 commit comments

Comments
 (0)
Please sign in to comment.