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