Skip to content

Commit 58da5e2

Browse files
authored
Merge pull request #161 from theburningmonk/master
Add support for other AWS services
2 parents e36299d + 647961f commit 58da5e2

File tree

3 files changed

+918
-74
lines changed

3 files changed

+918
-74
lines changed

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 211 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,241 @@ const _ = require('lodash');
33
const BbPromise = require('bluebird');
44
const 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 = /https:\/\/sqs.(.*).amazonaws.com\/(.*)\/(.*)/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+
6201
module.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(/"Resource":"([\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

Comments
 (0)