@@ -3,43 +3,241 @@ const _ = require('lodash');
3
3
const BbPromise = require ( 'bluebird' ) ;
4
4
const path = require ( 'path' ) ;
5
5
6
+ function getTaskStates ( states ) {
7
+ return _ . flatMap ( states , state => {
8
+ switch ( state . Type ) {
9
+ case 'Task' : {
10
+ return [ state ] ;
11
+ }
12
+ case 'Parallel' : {
13
+ const parallelStates = _ . flatMap ( state . Branches , branch => _ . values ( branch . States ) ) ;
14
+ return getTaskStates ( parallelStates ) ;
15
+ }
16
+ default : {
17
+ return [ ] ;
18
+ }
19
+ }
20
+ } ) ;
21
+ }
22
+
23
+ function sqsQueueUrlToArn ( serverless , queueUrl ) {
24
+ const regex = / h t t p s : \/ \/ s q s .( .* ) .a m a z o n a w s .c o m \/ ( .* ) \/ ( .* ) / g;
25
+ const match = regex . exec ( queueUrl ) ;
26
+ if ( match ) {
27
+ const region = match [ 1 ] ;
28
+ const accountId = match [ 2 ] ;
29
+ const queueName = match [ 3 ] ;
30
+ return `arn:aws:sqs:${ region } :${ accountId } :${ queueName } ` ;
31
+ }
32
+ serverless . cli . consoleLog ( `Unable to parse SQS queue url [${ queueUrl } ]` ) ;
33
+ return [ ] ;
34
+ }
35
+
36
+ function getSqsPermissions ( serverless , state ) {
37
+ if ( _ . has ( state , 'Parameters.QueueUrl' ) ||
38
+ _ . has ( state , [ 'Parameters' , 'QueueUrl.$' ] ) ) {
39
+ // if queue URL is provided by input, then need pervasive permissions (i.e. '*')
40
+ const queueArn = state . Parameters [ 'QueueUrl.$' ]
41
+ ? '*'
42
+ : sqsQueueUrlToArn ( serverless , state . Parameters . QueueUrl ) ;
43
+ return [ { action : 'sqs:SendMessage' , resource : queueArn } ] ;
44
+ }
45
+ serverless . cli . consoleLog ( 'SQS task missing Parameters.QueueUrl or Parameters.QueueUrl.$' ) ;
46
+ return [ ] ;
47
+ }
48
+
49
+ function getSnsPermissions ( serverless , state ) {
50
+ if ( _ . has ( state , 'Parameters.TopicArn' ) ||
51
+ _ . has ( state , [ 'Parameters' , 'TopicArn.$' ] ) ) {
52
+ // if topic ARN is provided by input, then need pervasive permissions
53
+ const topicArn = state . Parameters [ 'TopicArn.$' ] ? '*' : state . Parameters . TopicArn ;
54
+ return [ { action : 'sns:Publish' , resource : topicArn } ] ;
55
+ }
56
+ serverless . cli . consoleLog ( 'SNS task missing Parameters.TopicArn or Parameters.TopicArn.$' ) ;
57
+ return [ ] ;
58
+ }
59
+
60
+ function getDynamoDBArn ( tableName ) {
61
+ return {
62
+ 'Fn::Join' : [
63
+ ':' ,
64
+ [
65
+ 'arn:aws:dynamodb' ,
66
+ { Ref : 'AWS::Region' } ,
67
+ { Ref : 'AWS::AccountId' } ,
68
+ `table/${ tableName } ` ,
69
+ ] ,
70
+ ] ,
71
+ } ;
72
+ }
73
+
74
+ function getBatchPermissions ( ) {
75
+ return [ {
76
+ action : 'batch:SubmitJob,batch:DescribeJobs,batch:TerminateJob' ,
77
+ resource : '*' ,
78
+ } , {
79
+ action : 'events:PutTargets,events:PutRule,events:DescribeRule' ,
80
+ resource : {
81
+ 'Fn::Join' : [
82
+ ':' ,
83
+ [
84
+ 'arn:aws:events' ,
85
+ { Ref : 'AWS::Region' } ,
86
+ { Ref : 'AWS::AccountId' } ,
87
+ 'rules/StepFunctionsGetEventsForBatchJobsRule' ,
88
+ ] ,
89
+ ] ,
90
+ } ,
91
+ } ] ;
92
+ }
93
+
94
+ function getEcsPermissions ( ) {
95
+ return [ {
96
+ action : 'ecs:RunTask,ecs:StopTask,ecs:DescribeTasks' ,
97
+ resource : '*' ,
98
+ } , {
99
+ action : 'events:PutTargets,events:PutRule,events:DescribeRule' ,
100
+ resource : {
101
+ 'Fn::Join' : [
102
+ ':' ,
103
+ [
104
+ 'arn:aws:events' ,
105
+ { Ref : 'AWS::Region' } ,
106
+ { Ref : 'AWS::AccountId' } ,
107
+ 'rules/StepFunctionsGetEventsForECSTaskRule' ,
108
+ ] ,
109
+ ] ,
110
+ } ,
111
+ } ] ;
112
+ }
113
+
114
+ function getDynamoDBPermissions ( action , state ) {
115
+ const tableArn = state . Parameters [ 'TableName.$' ]
116
+ ? '*'
117
+ : getDynamoDBArn ( state . Parameters . TableName ) ;
118
+
119
+ return [ {
120
+ action,
121
+ resource : tableArn ,
122
+ } ] ;
123
+ }
124
+
125
+ // if there are multiple permissions with the same action, then collapsed them into one
126
+ // permission instead, and collect the resources into an array
127
+ function consolidatePermissionsByAction ( permissions ) {
128
+ return _ . chain ( permissions )
129
+ . groupBy ( perm => perm . action )
130
+ . mapValues ( perms => {
131
+ // find the unique resources
132
+ let resources = _ . uniqWith ( _ . flatMap ( perms , p => p . resource ) , _ . isEqual ) ;
133
+ if ( resources . includes ( '*' ) ) {
134
+ resources = '*' ;
135
+ }
136
+
137
+ return {
138
+ action : perms [ 0 ] . action ,
139
+ resource : resources ,
140
+ } ;
141
+ } )
142
+ . values ( )
143
+ . value ( ) ; // unchain
144
+ }
145
+
146
+ function consolidatePermissionsByResource ( permissions ) {
147
+ return _ . chain ( permissions )
148
+ . groupBy ( p => JSON . stringify ( p . resource ) )
149
+ . mapValues ( perms => {
150
+ // find unique actions
151
+ const actions = _ . uniq ( _ . flatMap ( perms , p => p . action . split ( ',' ) ) ) ;
152
+
153
+ return {
154
+ action : actions . join ( ',' ) ,
155
+ resource : perms [ 0 ] . resource ,
156
+ } ;
157
+ } )
158
+ . values ( )
159
+ . value ( ) ; // unchain
160
+ }
161
+
162
+ function getIamPermissions ( serverless , taskStates ) {
163
+ return _ . flatMap ( taskStates , state => {
164
+ switch ( state . Resource ) {
165
+ case 'arn:aws:states:::sqs:sendMessage' :
166
+ return getSqsPermissions ( serverless , state ) ;
167
+
168
+ case 'arn:aws:states:::sns:publish' :
169
+ return getSnsPermissions ( serverless , state ) ;
170
+
171
+ case 'arn:aws:states:::dynamodb:updateItem' :
172
+ return getDynamoDBPermissions ( 'dynamodb:UpdateItem' , state ) ;
173
+ case 'arn:aws:states:::dynamodb:putItem' :
174
+ return getDynamoDBPermissions ( 'dynamodb:PutItem' , state ) ;
175
+ case 'arn:aws:states:::dynamodb:getItem' :
176
+ return getDynamoDBPermissions ( 'dynamodb:GetItem' , state ) ;
177
+ case 'arn:aws:states:::dynamodb:deleteItem' :
178
+ return getDynamoDBPermissions ( 'dynamodb:DeleteItem' , state ) ;
179
+
180
+ case 'arn:aws:states:::batch:submitJob.sync' :
181
+ case 'arn:aws:states:::batch:submitJob' :
182
+ return getBatchPermissions ( ) ;
183
+
184
+ case 'arn:aws:states:::ecs:runTask.sync' :
185
+ case 'arn:aws:states:::ecs:runTask' :
186
+ return getEcsPermissions ( ) ;
187
+
188
+ default :
189
+ if ( state . Resource . startsWith ( 'arn:aws:lambda' ) ) {
190
+ return [ {
191
+ action : 'lambda:InvokeFunction' ,
192
+ resource : state . Resource ,
193
+ } ] ;
194
+ }
195
+ serverless . cli . consoleLog ( 'Cannot generate IAM policy statement for Task state' , state ) ;
196
+ return [ ] ;
197
+ }
198
+ } ) ;
199
+ }
200
+
6
201
module . exports = {
7
202
compileIamRole ( ) {
8
203
const customRolesProvided = [ ] ;
9
- let functionArns = [ ] ;
204
+ let iamPermissions = [ ] ;
10
205
this . getAllStateMachines ( ) . forEach ( ( stateMachineName ) => {
11
206
const stateMachineObj = this . getStateMachine ( stateMachineName ) ;
12
207
customRolesProvided . push ( 'role' in stateMachineObj ) ;
13
208
14
- const stateMachineJson = JSON . stringify ( stateMachineObj ) ;
15
- const regex = new RegExp ( / " R e s o u r c e " : " ( [ \w \- : * # { } . $ ] * ) " / gi) ;
16
- let match = regex . exec ( stateMachineJson ) ;
17
- while ( match !== null ) {
18
- functionArns . push ( match [ 1 ] ) ;
19
- match = regex . exec ( stateMachineJson ) ;
20
- }
209
+ const taskStates = getTaskStates ( stateMachineObj . definition . States ) ;
210
+ iamPermissions = iamPermissions . concat ( getIamPermissions ( this . serverless , taskStates ) ) ;
21
211
} ) ;
22
212
if ( _ . isEqual ( _ . uniq ( customRolesProvided ) , [ true ] ) ) {
23
213
return BbPromise . resolve ( ) ;
24
214
}
25
- functionArns = _ . uniq ( functionArns ) ;
26
215
27
- let iamRoleStateMachineExecutionTemplate = this . serverless . utils . readFileSync (
216
+ const iamRoleStateMachineExecutionTemplate = this . serverless . utils . readFileSync (
28
217
path . join ( __dirname ,
29
218
'..' ,
30
219
'..' ,
31
220
'iam-role-statemachine-execution-template.txt' )
32
221
) ;
33
222
34
- iamRoleStateMachineExecutionTemplate =
223
+ iamPermissions = consolidatePermissionsByAction ( iamPermissions ) ;
224
+ iamPermissions = consolidatePermissionsByResource ( iamPermissions ) ;
225
+
226
+ const iamStatements = iamPermissions . map ( p => ( {
227
+ Effect : 'Allow' ,
228
+ Action : p . action . split ( ',' ) ,
229
+ Resource : p . resource ,
230
+ } ) ) ;
231
+
232
+ const iamRoleJson =
35
233
iamRoleStateMachineExecutionTemplate
36
234
. replace ( '[region]' , this . options . region )
37
235
. replace ( '[PolicyName]' , this . getStateMachinePolicyName ( ) )
38
- . replace ( '[functions ]' , JSON . stringify ( functionArns ) ) ;
236
+ . replace ( '[Statements ]' , JSON . stringify ( iamStatements ) ) ;
39
237
40
238
const iamRoleStateMachineLogicalId = this . getiamRoleStateMachineLogicalId ( ) ;
41
239
const newIamRoleStateMachineExecutionObject = {
42
- [ iamRoleStateMachineLogicalId ] : JSON . parse ( iamRoleStateMachineExecutionTemplate ) ,
240
+ [ iamRoleStateMachineLogicalId ] : JSON . parse ( iamRoleJson ) ,
43
241
} ;
44
242
45
243
_ . merge ( this . serverless . service . provider . compiledCloudFormationTemplate . Resources ,
0 commit comments