@@ -33,7 +33,23 @@ function randomTargetId(stateMachineName, status) {
3333 return `${ stateMachineName } -${ status } -${ suffix } ` ;
3434}
3535
36- function compileTarget ( stateMachineName , status , targetObj ) {
36+ function randomLogicalId ( prefix ) {
37+ const suffix = chance . string ( {
38+ length : 5 ,
39+ pool : 'ABCDEFGHIJKLMNOPQRSTUFWXYZ' ,
40+ } ) ;
41+ return `${ prefix } ${ suffix } ` ;
42+ }
43+
44+ function randomPolicyName ( status , targetType ) {
45+ const suffix = chance . string ( {
46+ length : 5 ,
47+ pool : 'abcdefghijklmnopqrstufwxyzABCDEFGHIJKLMNOPQRSTUFWXYZ' ,
48+ } ) ;
49+ return `${ status } -${ targetType } -${ suffix } ` ;
50+ }
51+
52+ function compileTarget ( stateMachineName , status , targetObj , iamRoleLogicalId ) {
3753 // SQS and Kinesis are special cases as they can have additional props
3854 if ( _ . has ( targetObj , 'sqs.arn' ) ) {
3955 return {
@@ -51,7 +67,16 @@ function compileTarget(stateMachineName, status, targetObj) {
5167 PartitionKeyPath : targetObj . kinesis . partitionKeyPath ,
5268 } ,
5369 } ;
70+ } else if ( _ . has ( targetObj , 'stepFunctions' ) ) {
71+ return {
72+ Arn : targetObj . stepFunctions ,
73+ Id : randomTargetId ( stateMachineName , status ) ,
74+ RoleArn : {
75+ 'Fn::GetAtt' : [ iamRoleLogicalId , 'Arn' ] ,
76+ } ,
77+ } ;
5478 }
79+
5580 const targetType = supportedTargets . find ( t => _ . has ( targetObj , t ) ) ;
5681 const arn = _ . get ( targetObj , targetType ) ;
5782 return {
@@ -60,24 +85,110 @@ function compileTarget(stateMachineName, status, targetObj) {
6085 } ;
6186}
6287
63- function compileIamPermission ( targetObj ) {
64- const targetType = supportedTargets . find ( t => _ . has ( targetObj , t ) ) ;
65- const action = targetPermissions [ targetType ] ;
88+ function compileSnsPolicy ( status , snsTarget ) {
89+ return {
90+ Type : 'AWS::SNS::TopicPolicy' ,
91+ Properties : {
92+ PolicyDocument : {
93+ Version : '2012-10-17' ,
94+ Statement : {
95+ Sid : randomPolicyName ( status , 'sns' ) ,
96+ Principal : {
97+ Service : 'events.amazonaws.com' ,
98+ } ,
99+ Effect : 'Allow' ,
100+ Action : 'sns:Publish' ,
101+ Resource : snsTarget ,
102+ } ,
103+ } ,
104+ Topics : [ snsTarget ] ,
105+ } ,
106+ } ;
107+ }
66108
67- // SQS and Kinesis are special cases as they can have additional props
68- if ( _ . has ( targetObj , 'sqs.arn' ) ) {
109+ function convertToQueueUrl ( sqsArn ) {
110+ if ( _ . isString ( sqsArn ) ) {
111+ const segments = sqsArn . split ( ':' ) ;
112+ const queueName = _ . last ( segments ) ;
69113 return {
70- action,
71- resource : _ . get ( targetObj , 'sqs.arn' ) ,
114+ 'Fn::Sub' : [
115+ 'https://sqs.${AWS::Region}.amazonaws.com/${AWS::AccountId}/${QueueName}' ,
116+ { QueueName : queueName } ,
117+ ] ,
72118 } ;
73- } else if ( _ . has ( targetObj , 'kinesis.arn' ) ) {
119+ } else if ( sqsArn [ 'Fn::GetAtt' ] ) {
120+ const logicalId = sqsArn [ 'Fn::GetAtt' ] [ 0 ] ;
121+ return { Ref : logicalId } ;
122+ }
123+ throw new Error (
124+ `Unable to convert SQS ARN [${ sqsArn } ] to SQS Url. ` +
125+ 'This is required for setting up Step Functions notifications to SQS. ' +
126+ 'Try using Fn::GetAtt when setting the SQS arn.' ) ;
127+ }
128+
129+ function compileSqsPolicy ( status , sqsTarget ) {
130+ return {
131+ Type : 'AWS::SQS::QueuePolicy' ,
132+ Properties : {
133+ PolicyDocument : {
134+ Version : '2012-10-17' ,
135+ Statement : {
136+ Sid : randomPolicyName ( status , 'sqs' ) ,
137+ Principal : {
138+ Service : 'events.amazonaws.com' ,
139+ } ,
140+ Effect : 'Allow' ,
141+ Action : 'sqs:SendMessage' ,
142+ Resource : sqsTarget ,
143+ } ,
144+ } ,
145+ Queues : [ convertToQueueUrl ( sqsTarget ) ] ,
146+ } ,
147+ } ;
148+ }
149+
150+ function compileLambdaPermission ( lambdaTarget ) {
151+ return {
152+ Type : 'AWS::Lambda::Permission' ,
153+ Properties : {
154+ Action : 'lambda:InvokeFunction' ,
155+ FunctionName : lambdaTarget ,
156+ Principal : 'events.amazonaws.com' ,
157+ } ,
158+ } ;
159+ }
160+
161+ function compilePermissionForTarget ( status , targetObj ) {
162+ if ( targetObj . sns ) {
163+ return {
164+ type : 'policy' ,
165+ resource : compileSnsPolicy ( status , targetObj . sns ) ,
166+ } ;
167+ } else if ( targetObj . sqs ) {
168+ const arn = _ . get ( targetObj , 'sqs.arn' , targetObj . sqs ) ;
169+ return {
170+ type : 'policy' ,
171+ resource : compileSqsPolicy ( status , arn ) ,
172+ } ;
173+ } else if ( targetObj . kinesis ) {
174+ const arn = _ . get ( targetObj , 'kinesis.arn' , targetObj . kinesis ) ;
175+ return {
176+ type : 'iam' ,
177+ action : 'kinesis:PutRecord' ,
178+ resource : arn ,
179+ } ;
180+ } else if ( targetObj . lambda ) {
74181 return {
75- action ,
76- resource : _ . get ( targetObj , 'kinesis.arn' ) ,
182+ type : 'policy' ,
183+ resource : compileLambdaPermission ( targetObj . lambda ) ,
77184 } ;
78185 }
79186
187+ const targetType = supportedTargets . find ( t => _ . has ( targetObj , t ) ) ;
188+ const action = targetPermissions [ targetType ] ;
189+
80190 return {
191+ type : 'iam' ,
81192 action,
82193 resource : targetObj [ targetType ] ,
83194 } ;
@@ -96,42 +207,74 @@ function bootstrapIamRole() {
96207 } ,
97208 } ,
98209 } ,
99- Policies : [
100- {
101- PolicyName : 'root' ,
102- PolicyDocument : {
103- Version : '2012-10-17' ,
104- Statement : [ ] ,
105- } ,
106- } ,
107- ] ,
210+ Policies : [ ] ,
108211 } ,
109212 } ;
110- const addPermission = ( action , resource ) => {
111- iamRole . Properties . Policies [ 0 ] . PolicyDocument . Statement . push ( {
112- Effect : 'Allow' ,
113- Action : action ,
114- Resource : resource ,
213+ const addPolicy = ( name , action , resource ) => {
214+ iamRole . Properties . Policies . push ( {
215+ PolicyName : name ,
216+ PolicyDocument : {
217+ Version : '2012-10-17' ,
218+ Statement : [ {
219+ Effect : 'Allow' ,
220+ Action : action ,
221+ Resource : resource ,
222+ } ] ,
223+ } ,
115224 } ) ;
116225 } ;
117226
118- return { iamRole, addPermission } ;
227+ return { iamRole, addPolicy } ;
228+ }
229+
230+ function * compilePermissionResources ( stateMachineLogicalId , iamRoleLogicalId , targets ) {
231+ const { iamRole, addPolicy } = bootstrapIamRole ( ) ;
232+
233+ for ( const { status, target } of targets ) {
234+ const perm = compilePermissionForTarget ( status , target ) ;
235+ if ( perm . type === 'iam' ) {
236+ const targetType = _ . keys ( target ) [ 0 ] ;
237+ addPolicy (
238+ randomPolicyName ( status , targetType ) ,
239+ perm . action ,
240+ perm . resource ) ;
241+ } else if ( perm . type === 'policy' ) {
242+ yield {
243+ logicalId : randomLogicalId ( `${ stateMachineLogicalId } ResourcePolicy` ) ,
244+ resource : perm . resource ,
245+ } ;
246+ }
247+ }
248+
249+ if ( ! _ . isEmpty ( iamRole . Properties . Policies ) ) {
250+ yield {
251+ logicalId : iamRoleLogicalId ,
252+ resource : iamRole ,
253+ } ;
254+ }
119255}
120256
121257function * compileResources ( stateMachineLogicalId , stateMachineName , notificationsObj ) {
122258 const iamRoleLogicalId = `${ stateMachineLogicalId } NotificationsIamRole` ;
123- const { iamRole, addPermission } = bootstrapIamRole ( ) ;
259+ const allTargets = _ . flatMap ( executionStatuses , status =>
260+ _ . get ( notificationsObj , status , [ ] ) . map ( target => ( { status, target } ) ) ) ;
261+ const permissions = compilePermissionResources (
262+ stateMachineLogicalId , iamRoleLogicalId , allTargets ) ;
263+ const permissionResources = Array . from ( permissions ) ;
264+ for ( const { logicalId, resource } of permissionResources ) {
265+ yield [ logicalId , resource ] ;
266+ }
267+
268+ const needRoleArn = permissionResources . some ( ( { logicalId } ) => logicalId === iamRoleLogicalId ) ;
269+ const roleArn = needRoleArn
270+ ? { 'Fn::GetAtt' : [ iamRoleLogicalId , 'Arn' ] }
271+ : undefined ;
124272
125273 for ( const status of executionStatuses ) {
126274 const targets = notificationsObj [ status ] ;
127275 if ( ! _ . isEmpty ( targets ) ) {
128- const cfnTargets = targets
129- . map ( t => compileTarget ( stateMachineName , status , t ) )
130- . filter ( _ . isObjectLike ) ;
131- targets
132- . map ( compileIamPermission )
133- . filter ( _ . isObjectLike )
134- . forEach ( ( { action, resource } ) => addPermission ( action , resource ) ) ;
276+ const cfnTargets = targets . map ( t =>
277+ compileTarget ( stateMachineName , status , t , iamRoleLogicalId ) ) ;
135278
136279 const eventRuleLogicalId =
137280 `${ stateMachineLogicalId } Notifications${ status . replace ( '_' , '' ) } EventRule` ;
@@ -144,22 +287,19 @@ function* compileResources(stateMachineLogicalId, stateMachineName, notification
144287 'detail-type' : [ 'Step Functions Execution Status Change' ] ,
145288 detail : {
146289 status : [ status ] ,
290+ stateMachineArn : [ {
291+ Ref : stateMachineLogicalId ,
292+ } ] ,
147293 } ,
148294 } ,
149295 Name : `${ stateMachineName } -${ status } -notification` ,
150- RoleArn : {
151- 'Fn::GetAtt' : [ iamRoleLogicalId , 'Arn' ] ,
152- } ,
296+ RoleArn : roleArn ,
153297 Targets : cfnTargets ,
154298 } ,
155299 } ;
156300 yield [ eventRuleLogicalId , eventRule ] ;
157301 }
158302 }
159-
160- if ( ! _ . isEmpty ( iamRole . Properties . Policies [ 0 ] . PolicyDocument . Statement ) ) {
161- yield [ iamRoleLogicalId , iamRole ] ;
162- }
163303}
164304
165305function validateConfig ( serverless , stateMachineName , notificationsObj ) {
0 commit comments