@@ -3,43 +3,241 @@ 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+ }
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+
6201module . exports = {
7202 compileIamRole ( ) {
8203 const customRolesProvided = [ ] ;
9- let functionArns = [ ] ;
204+ let iamPermissions = [ ] ;
10205 this . getAllStateMachines ( ) . forEach ( ( stateMachineName ) => {
11206 const stateMachineObj = this . getStateMachine ( stateMachineName ) ;
12207 customRolesProvided . push ( 'role' in stateMachineObj ) ;
13208
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 ) ) ;
21211 } ) ;
22212 if ( _ . isEqual ( _ . uniq ( customRolesProvided ) , [ true ] ) ) {
23213 return BbPromise . resolve ( ) ;
24214 }
25- functionArns = _ . uniq ( functionArns ) ;
26215
27- let iamRoleStateMachineExecutionTemplate = this . serverless . utils . readFileSync (
216+ const iamRoleStateMachineExecutionTemplate = this . serverless . utils . readFileSync (
28217 path . join ( __dirname ,
29218 '..' ,
30219 '..' ,
31220 'iam-role-statemachine-execution-template.txt' )
32221 ) ;
33222
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 =
35233 iamRoleStateMachineExecutionTemplate
36234 . replace ( '[region]' , this . options . region )
37235 . replace ( '[PolicyName]' , this . getStateMachinePolicyName ( ) )
38- . replace ( '[functions ]' , JSON . stringify ( functionArns ) ) ;
236+ . replace ( '[Statements ]' , JSON . stringify ( iamStatements ) ) ;
39237
40238 const iamRoleStateMachineLogicalId = this . getiamRoleStateMachineLogicalId ( ) ;
41239 const newIamRoleStateMachineExecutionObject = {
42- [ iamRoleStateMachineLogicalId ] : JSON . parse ( iamRoleStateMachineExecutionTemplate ) ,
240+ [ iamRoleStateMachineLogicalId ] : JSON . parse ( iamRoleJson ) ,
43241 } ;
44242
45243 _ . merge ( this . serverless . service . provider . compiledCloudFormationTemplate . Resources ,
0 commit comments