Skip to content

Commit d59bee9

Browse files
authored
fix(apigateway): StepFunctionsIntegration does not create required role and responses (#19486)
The method responses and role were created automatically **only** when using the `StepFunctionsRestApi` construct. Move the logic inside the integration. It's now possible to do: ```ts api.root.addResource('sfn').addMethod('POST', StepFunctionsIntegration.startExecution(stateMachine)); ``` Previously this did not create the proper method responses and required a role to be passed. ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](../CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](../CONTRIBUTING.md/#adding-new-unconventional-dependencies) *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 01b538e commit d59bee9

File tree

5 files changed

+154
-65
lines changed

5 files changed

+154
-65
lines changed

packages/@aws-cdk/aws-apigateway/lib/integrations/stepfunctions.ts

+42-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Token } from '@aws-cdk/core';
66
import { RequestContext } from '.';
77
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration';
88
import { Method } from '../method';
9+
import { Model } from '../model';
910
import { AwsIntegration } from './aws';
1011
/**
1112
* Options when configuring Step Functions synchronous integration with Rest API
@@ -94,6 +95,7 @@ export class StepFunctionsIntegration {
9495
* @example
9596
*
9697
* const stateMachine = new stepfunctions.StateMachine(this, 'MyStateMachine', {
98+
* stateMachineType: stepfunctions.StateMachineType.EXPRESS,
9799
* definition: stepfunctions.Chain.start(new stepfunctions.Pass(this, 'Pass')),
98100
* });
99101
*
@@ -127,9 +129,11 @@ class StepFunctionsExecutionIntegration extends AwsIntegration {
127129

128130
public bind(method: Method): IntegrationConfig {
129131
const bindResult = super.bind(method);
130-
const principal = new iam.ServicePrincipal('apigateway.amazonaws.com');
131132

132-
this.stateMachine.grantExecution(principal, 'states:StartSyncExecution');
133+
const credentialsRole = bindResult.options?.credentialsRole ?? new iam.Role(method, 'StartSyncExecutionRole', {
134+
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
135+
});
136+
this.stateMachine.grantStartSyncExecution(credentialsRole);
133137

134138
let stateMachineName;
135139

@@ -152,8 +156,17 @@ class StepFunctionsExecutionIntegration extends AwsIntegration {
152156
if (stateMachineName !== undefined && !Token.isUnresolved(stateMachineName)) {
153157
deploymentToken = JSON.stringify({ stateMachineName });
154158
}
159+
160+
for (const methodResponse of METHOD_RESPONSES) {
161+
method.addMethodResponse(methodResponse);
162+
}
163+
155164
return {
156165
...bindResult,
166+
options: {
167+
...bindResult.options,
168+
credentialsRole,
169+
},
157170
deploymentToken,
158171
};
159172
}
@@ -200,8 +213,8 @@ function integrationResponse() {
200213
/* eslint-disable */
201214
'application/json': [
202215
'#set($inputRoot = $input.path(\'$\'))',
203-
'#if($input.path(\'$.status\').toString().equals("FAILED"))',
204-
'#set($context.responseOverride.status = 500)',
216+
'#if($input.path(\'$.status\').toString().equals("FAILED"))',
217+
'#set($context.responseOverride.status = 500)',
205218
'{',
206219
'"error": "$input.path(\'$.error\')",',
207220
'"cause": "$input.path(\'$.cause\')"',
@@ -301,4 +314,28 @@ function requestContext(requestContextObj: RequestContext | undefined): string {
301314
const doublequotes = '"';
302315
const replaceWith = '@@';
303316
return contextAsString.split(doublequotes).join(replaceWith);
304-
}
317+
}
318+
319+
/**
320+
* Method response model for each HTTP code response
321+
*/
322+
const METHOD_RESPONSES = [
323+
{
324+
statusCode: '200',
325+
responseModels: {
326+
'application/json': Model.EMPTY_MODEL,
327+
},
328+
},
329+
{
330+
statusCode: '400',
331+
responseModels: {
332+
'application/json': Model.ERROR_MODEL,
333+
},
334+
},
335+
{
336+
statusCode: '500',
337+
responseModels: {
338+
'application/json': Model.ERROR_MODEL,
339+
},
340+
},
341+
];

packages/@aws-cdk/aws-apigateway/lib/method.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ArnFormat, Resource, Stack } from '@aws-cdk/core';
1+
import { ArnFormat, Lazy, Resource, Stack } from '@aws-cdk/core';
22
import { Construct } from 'constructs';
33
import { CfnMethod, CfnMethodProps } from './apigateway.generated';
44
import { Authorizer, IAuthorizer } from './authorizer';
@@ -168,6 +168,8 @@ export class Method extends Resource {
168168
*/
169169
public readonly api: IRestApi;
170170

171+
private methodResponses: MethodResponse[];
172+
171173
constructor(scope: Construct, id: string, props: MethodProps) {
172174
super(scope, id);
173175

@@ -196,6 +198,8 @@ export class Method extends Resource {
196198
authorizer._attachToApi(this.api);
197199
}
198200

201+
this.methodResponses = options.methodResponses ?? [];
202+
199203
const integration = props.integration ?? this.resource.defaultIntegration ?? new MockIntegration();
200204
const bindResult = integration.bind(this);
201205

@@ -209,7 +213,7 @@ export class Method extends Resource {
209213
authorizerId,
210214
requestParameters: options.requestParameters || defaultMethodOptions.requestParameters,
211215
integration: this.renderIntegration(bindResult),
212-
methodResponses: this.renderMethodResponses(options.methodResponses),
216+
methodResponses: Lazy.any({ produce: () => this.renderMethodResponses(this.methodResponses) }, { omitEmptyArray: true }),
213217
requestModels: this.renderRequestModels(options.requestModels),
214218
requestValidatorId: this.requestValidatorId(options),
215219
authorizationScopes: options.authorizationScopes ?? defaultMethodOptions.authorizationScopes,
@@ -267,6 +271,13 @@ export class Method extends Resource {
267271
return this.api.arnForExecuteApi(this.httpMethod, pathForArn(this.resource.path), 'test-invoke-stage');
268272
}
269273

274+
/**
275+
* Add a method response to this method
276+
*/
277+
public addMethodResponse(methodResponse: MethodResponse): void {
278+
this.methodResponses.push(methodResponse);
279+
}
280+
270281
private renderIntegration(bindResult: IntegrationConfig): CfnMethod.IntegrationProperty {
271282
const options = bindResult.options ?? {};
272283
let credentials;

packages/@aws-cdk/aws-apigateway/lib/stepfunctions-api.ts

+10-51
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Construct } from 'constructs';
44
import { RestApi, RestApiProps } from '.';
55
import { RequestContext } from './integrations';
66
import { StepFunctionsIntegration } from './integrations/stepfunctions';
7-
import { Model } from './model';
87

98
/**
109
* Properties for StepFunctionsRestApi
@@ -89,6 +88,14 @@ export interface StepFunctionsRestApiProps extends RestApiProps {
8988
* @default false
9089
*/
9190
readonly authorizer?: boolean;
91+
92+
/**
93+
* An IAM role that API Gateway will assume to start the execution of the
94+
* state machine.
95+
*
96+
* @default - a new role is created
97+
*/
98+
readonly role?: iam.IRole;
9299
}
93100

94101
/**
@@ -105,7 +112,7 @@ export class StepFunctionsRestApi extends RestApi {
105112
}
106113

107114
const stepfunctionsIntegration = StepFunctionsIntegration.startExecution(props.stateMachine, {
108-
credentialsRole: role(scope, props),
115+
credentialsRole: props.role,
109116
requestContext: props.requestContext,
110117
path: props.path?? true,
111118
querystring: props.querystring?? true,
@@ -115,54 +122,6 @@ export class StepFunctionsRestApi extends RestApi {
115122

116123
super(scope, id, props);
117124

118-
this.root.addMethod('ANY', stepfunctionsIntegration, {
119-
methodResponses: methodResponse(),
120-
});
125+
this.root.addMethod('ANY', stepfunctionsIntegration);
121126
}
122127
}
123-
124-
/**
125-
* Defines the IAM Role for API Gateway with required permissions
126-
* to invoke a synchronous execution for the provided state machine
127-
*
128-
* @param scope
129-
* @param props
130-
* @returns Role - IAM Role
131-
*/
132-
function role(scope: Construct, props: StepFunctionsRestApiProps): iam.Role {
133-
const roleName: string = 'StartSyncExecutionRole';
134-
const apiRole = new iam.Role(scope, roleName, {
135-
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
136-
});
137-
138-
props.stateMachine.grantStartSyncExecution(apiRole);
139-
140-
return apiRole;
141-
}
142-
143-
/**
144-
* Defines the method response modelfor each HTTP code response
145-
* @returns methodResponse
146-
*/
147-
function methodResponse() {
148-
return [
149-
{
150-
statusCode: '200',
151-
responseModels: {
152-
'application/json': Model.EMPTY_MODEL,
153-
},
154-
},
155-
{
156-
statusCode: '400',
157-
responseModels: {
158-
'application/json': Model.ERROR_MODEL,
159-
},
160-
},
161-
{
162-
statusCode: '500',
163-
responseModels: {
164-
'application/json': Model.ERROR_MODEL,
165-
},
166-
},
167-
];
168-
}

packages/@aws-cdk/aws-apigateway/test/integ.stepfunctions-api.expected.json

+6-6
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"StateMachineRoleB840431D"
4545
]
4646
},
47-
"StartSyncExecutionRoleDE73CB90": {
47+
"StepFunctionsRestApiANYStartSyncExecutionRole425C03BB": {
4848
"Type": "AWS::IAM::Role",
4949
"Properties": {
5050
"AssumeRolePolicyDocument": {
@@ -61,7 +61,7 @@
6161
}
6262
}
6363
},
64-
"StartSyncExecutionRoleDefaultPolicy5A5803F8": {
64+
"StepFunctionsRestApiANYStartSyncExecutionRoleDefaultPolicy7B6D0CED": {
6565
"Type": "AWS::IAM::Policy",
6666
"Properties": {
6767
"PolicyDocument": {
@@ -76,10 +76,10 @@
7676
],
7777
"Version": "2012-10-17"
7878
},
79-
"PolicyName": "StartSyncExecutionRoleDefaultPolicy5A5803F8",
79+
"PolicyName": "StepFunctionsRestApiANYStartSyncExecutionRoleDefaultPolicy7B6D0CED",
8080
"Roles": [
8181
{
82-
"Ref": "StartSyncExecutionRoleDE73CB90"
82+
"Ref": "StepFunctionsRestApiANYStartSyncExecutionRole425C03BB"
8383
}
8484
]
8585
}
@@ -152,7 +152,7 @@
152152
"Integration": {
153153
"Credentials": {
154154
"Fn::GetAtt": [
155-
"StartSyncExecutionRoleDE73CB90",
155+
"StepFunctionsRestApiANYStartSyncExecutionRole425C03BB",
156156
"Arn"
157157
]
158158
},
@@ -289,4 +289,4 @@
289289
}
290290
}
291291
}
292-
}
292+
}

packages/@aws-cdk/aws-apigateway/test/stepfunctions-api.test.ts

+83-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as sfn from '@aws-cdk/aws-stepfunctions';
33
import { StateMachine } from '@aws-cdk/aws-stepfunctions';
44
import * as cdk from '@aws-cdk/core';
55
import * as apigw from '../lib';
6+
import { StepFunctionsIntegration } from '../lib';
67

78
describe('Step Functions api', () => {
89
test('StepFunctionsRestApi defines correct REST API resources', () => {
@@ -33,7 +34,7 @@ describe('Step Functions api', () => {
3334
Integration: {
3435
Credentials: {
3536
'Fn::GetAtt': [
36-
'StartSyncExecutionRoleDE73CB90',
37+
'StepFunctionsRestApiANYStartSyncExecutionRole425C03BB',
3738
'Arn',
3839
],
3940
},
@@ -75,6 +76,87 @@ describe('Step Functions api', () => {
7576
});
7677
});
7778

79+
test('StepFunctionsExecutionIntegration on a method', () => {
80+
// GIVEN
81+
const stack = new cdk.Stack();
82+
const api = new apigw.RestApi(stack, 'Api');
83+
const stateMachine = new sfn.StateMachine(stack, 'StateMachine', {
84+
stateMachineType: sfn.StateMachineType.EXPRESS,
85+
definition: new sfn.Pass(stack, 'Pass'),
86+
});
87+
88+
// WHEN
89+
api.root.addResource('sfn').addMethod('POST', StepFunctionsIntegration.startExecution(stateMachine));
90+
91+
// THEN
92+
Template.fromStack(stack).hasResourceProperties('AWS::ApiGateway::Method', {
93+
HttpMethod: 'POST',
94+
MethodResponses: getMethodResponse(),
95+
Integration: {
96+
Credentials: {
97+
'Fn::GetAtt': [
98+
'ApisfnPOSTStartSyncExecutionRole8E8879B0',
99+
'Arn',
100+
],
101+
},
102+
IntegrationHttpMethod: 'POST',
103+
IntegrationResponses: getIntegrationResponse(),
104+
RequestTemplates: {
105+
'application/json': {
106+
'Fn::Join': [
107+
'',
108+
[
109+
"## 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($includeAuthorizer = false)\n#set($allParams = $input.params())\n{\n \"stateMachineArn\": \"",
110+
{
111+
Ref: 'StateMachine2E01A3A5',
112+
},
113+
"\",\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 #if ($includeAuthorizer)\n #set($inputString = \"$inputString, @@authorizer@@:{\")\n #foreach($paramName in $context.authorizer.keySet())\n #set($inputString = \"$inputString @@$paramName@@: @@$util.escapeJavaScript($context.authorizer.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",
114+
],
115+
],
116+
},
117+
},
118+
Type: 'AWS',
119+
Uri: {
120+
'Fn::Join': [
121+
'',
122+
[
123+
'arn:',
124+
{
125+
Ref: 'AWS::Partition',
126+
},
127+
':apigateway:',
128+
{
129+
Ref: 'AWS::Region',
130+
},
131+
':states:action/StartSyncExecution',
132+
],
133+
],
134+
},
135+
PassthroughBehavior: 'NEVER',
136+
},
137+
});
138+
139+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
140+
PolicyDocument: {
141+
Statement: [
142+
{
143+
Action: 'states:StartSyncExecution',
144+
Effect: 'Allow',
145+
Resource: {
146+
Ref: 'StateMachine2E01A3A5',
147+
},
148+
},
149+
],
150+
Version: '2012-10-17',
151+
},
152+
Roles: [
153+
{
154+
Ref: 'ApisfnPOSTStartSyncExecutionRole8E8879B0',
155+
},
156+
],
157+
});
158+
});
159+
78160
test('fails if options.defaultIntegration is set', () => {
79161
//GIVEN
80162
const { stack, stateMachine } = givenSetup();

0 commit comments

Comments
 (0)