Skip to content

Commit b163d81

Browse files
committed
feat: support s3:putobject in sdk and resultwriter integrations
1 parent 740557c commit b163d81

File tree

2 files changed

+168
-16
lines changed

2 files changed

+168
-16
lines changed

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ function getTaskStates(states, stateMachineName) {
3232
if (state.ItemReader) {
3333
taskStates.push(state.ItemReader);
3434
}
35+
if (state.ResultWriter) {
36+
taskStates.push(state.ResultWriter);
37+
}
3538
return taskStates;
3639
}
3740
default: {
@@ -429,22 +432,24 @@ function getEventBridgePermissions(state) {
429432
];
430433
}
431434

432-
function getS3GetObjectPermissions(state) {
433-
const bucket = state.Parameters['Bucket.$'] ? state.Parameters['Bucket.$'] : state.Parameters.Bucket;
434-
const key = state.Parameters['Key.$'] ? state.Parameters['Key.$'] : state.Parameters.Key;
435+
function getS3ObjectPermissions(action, state) {
436+
const bucket = state.Parameters.Bucket || '*';
437+
const key = state.Parameters.Key || '*';
438+
const prefix = state.Parameters.Prefix;
439+
let arn;
435440

436-
if (bucket.startsWith('$') && key.startsWith('$')) {
437-
return [{
438-
action: 's3:GetObject',
439-
resource: [
440-
'*',
441-
],
442-
}];
441+
if (prefix) {
442+
arn = `arn:aws:s3:::${bucket}/${prefix}/${key}`;
443+
} else if (bucket === '*' && key === '*') {
444+
arn = '*';
445+
} else {
446+
arn = `arn:aws:s3:::${bucket}/${key}`;
443447
}
448+
444449
return [{
445-
action: 's3:GetObject',
450+
action,
446451
resource: [
447-
`arn:aws:s3:::${bucket}/${key}`,
452+
arn,
448453
],
449454
}];
450455
}
@@ -564,7 +569,10 @@ function getIamPermissions(taskStates) {
564569

565570
case 'arn:aws:states:::s3:getObject':
566571
case 'arn:aws:states:::aws-sdk:s3:getObject':
567-
return getS3GetObjectPermissions(state);
572+
return getS3ObjectPermissions('s3:GetObject', state);
573+
case 'arn:aws:states:::s3:putObject':
574+
case 'arn:aws:states:::aws-sdk:s3:putObject':
575+
return getS3ObjectPermissions('s3:PutObject', state);
568576

569577
default:
570578
if (isIntrinsic(state.Resource) || !!state.Resource.match(/arn:aws(-[a-z]+)*:lambda/)) {

lib/deploy/stepFunctions/compileIamRole.test.js

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,7 +1452,7 @@ describe('#compileIamRole', () => {
14521452
expectDenyAllPolicy(policy);
14531453
});
14541454

1455-
it('should give s3:GetObject permission for only objects referenced by state machine', () => {
1455+
it('should give s3 permissions for only objects referenced by state machine', () => {
14561456
const hello = 'hello.txt';
14571457
const world = 'world.txt';
14581458
const testBucket = 'test-bucket';
@@ -1469,6 +1469,16 @@ describe('#compileIamRole', () => {
14691469
Bucket: bucket,
14701470
Key: key,
14711471
},
1472+
Next: 'B',
1473+
},
1474+
B: {
1475+
Type: 'Task',
1476+
Resource: 'arn:aws:states:::aws-sdk:s3:putObject',
1477+
Parameters: {
1478+
Bucket: bucket,
1479+
Key: key,
1480+
Body: {},
1481+
},
14721482
End: true,
14731483
},
14741484
},
@@ -1491,6 +1501,14 @@ describe('#compileIamRole', () => {
14911501
.to.be.deep.equal([`arn:aws:s3:::${testBucket}/${hello}`]);
14921502
expect(policy2.PolicyDocument.Statement[0].Resource)
14931503
.to.be.deep.equal([`arn:aws:s3:::${testBucket}/${world}`]);
1504+
1505+
[policy1, policy2].forEach((policy) => {
1506+
expect(policy.PolicyDocument.Statement[0].Action)
1507+
.to.be.deep.equal([
1508+
's3:GetObject',
1509+
's3:PutObject',
1510+
]);
1511+
});
14941512
});
14951513

14961514
it('should give s3:GetObject permission for only objects referenced by state machine with ItemReader', () => {
@@ -1612,9 +1630,135 @@ describe('#compileIamRole', () => {
16121630
.provider.compiledCloudFormationTemplate.Resources;
16131631
const policy1 = resources.StateMachine1Role.Properties.Policies[0];
16141632

1615-
// even though some tasks target specific topic ARNs, other states use Bucket.$
1633+
// even though some tasks target specific values, other states use Bucket.$
16161634
// and Key.$ so we need to give broad permissions to be able to get any
1617-
// table and key the input specifies
1635+
// bucket and key the input specifies
1636+
expect(policy1.PolicyDocument.Statement[1].Resource)
1637+
.to.be.deep.equal('*');
1638+
});
1639+
1640+
it('should give s3:PutObject permission for only objects referenced by state machine with ResultWriter', () => {
1641+
const hello = 'hello';
1642+
const world = 'world';
1643+
const testBucket = 'test-bucket';
1644+
1645+
const genStateMachine = (id, lambdaArn, bucket, prefix) => ({
1646+
id,
1647+
definition: {
1648+
StartAt: 'A',
1649+
States: {
1650+
A: {
1651+
Type: 'Map',
1652+
ItemProcessor: {
1653+
StartAt: 'B',
1654+
States: {
1655+
B: {
1656+
Type: 'Task',
1657+
Resource: lambdaArn,
1658+
End: true,
1659+
},
1660+
},
1661+
},
1662+
ResultWriter: {
1663+
Resource: 'arn:aws:states:::s3:putObject',
1664+
Parameters: {
1665+
Bucket: bucket,
1666+
Prefix: prefix,
1667+
},
1668+
},
1669+
End: true,
1670+
},
1671+
},
1672+
},
1673+
});
1674+
1675+
serverless.service.stepFunctions = {
1676+
stateMachines: {
1677+
myStateMachine1: genStateMachine('StateMachine1',
1678+
'arn:aws:lambda:us-west-2:1234567890:function:foo', testBucket, hello),
1679+
myStateMachine2: genStateMachine('StateMachine2',
1680+
'arn:aws:lambda:us-west-2:1234567890:function:foo', testBucket, world),
1681+
},
1682+
};
1683+
1684+
serverlessStepFunctions.compileIamRole();
1685+
const resources = serverlessStepFunctions.serverless.service
1686+
.provider.compiledCloudFormationTemplate.Resources;
1687+
const policy1 = resources.StateMachine1Role.Properties.Policies[0];
1688+
const policy2 = resources.StateMachine2Role.Properties.Policies[0];
1689+
expect(policy1.PolicyDocument.Statement[1].Resource)
1690+
.to.be.deep.equal([`arn:aws:s3:::${testBucket}/${hello}/*`]);
1691+
expect(policy2.PolicyDocument.Statement[1].Resource)
1692+
.to.be.deep.equal([`arn:aws:s3:::${testBucket}/${world}/*`]);
1693+
});
1694+
1695+
it('should give s3:PutObject permission to * when Bucket.$ and Prefix.$ are seen on ResultWriter', () => {
1696+
const genStateMachine = (id, lambdaArn) => ({
1697+
id,
1698+
definition: {
1699+
StartAt: 'A',
1700+
States: {
1701+
A: {
1702+
Type: 'Map',
1703+
ItemProcessor: {
1704+
StartAt: 'B',
1705+
States: {
1706+
B: {
1707+
Type: 'Task',
1708+
Resource: lambdaArn,
1709+
End: true,
1710+
},
1711+
},
1712+
},
1713+
ResultWriter: {
1714+
Resource: 'arn:aws:states:::s3:putObject',
1715+
Parameters: {
1716+
Bucket: 'test-bucket',
1717+
Prefix: 'test-prefix',
1718+
},
1719+
},
1720+
Next: 'C',
1721+
},
1722+
C: {
1723+
Type: 'Map',
1724+
ItemProcessor: {
1725+
StartAt: 'D',
1726+
States: {
1727+
D: {
1728+
Type: 'Task',
1729+
Resource: lambdaArn,
1730+
End: true,
1731+
},
1732+
},
1733+
},
1734+
ResultWriter: {
1735+
Resource: 'arn:aws:states:::s3:putObject',
1736+
Parameters: {
1737+
'Bucket.$': '$.testBucket',
1738+
'Prefix.$': '$.prefix',
1739+
},
1740+
},
1741+
End: true,
1742+
},
1743+
},
1744+
},
1745+
});
1746+
1747+
serverless.service.stepFunctions = {
1748+
stateMachines: {
1749+
myStateMachine1: genStateMachine('StateMachine1',
1750+
'arn:aws:lambda:us-west-2:1234567890:function:foo'),
1751+
},
1752+
};
1753+
1754+
serverlessStepFunctions.compileIamRole();
1755+
const resources = serverlessStepFunctions.serverless.service
1756+
.provider.compiledCloudFormationTemplate.Resources;
1757+
const policy1 = resources.StateMachine1Role.Properties.Policies[0];
1758+
1759+
// even though some tasks target specific values, other states use Bucket.$
1760+
// and Prefix.$ so we need to give broad permissions to be able to write to
1761+
// any bucket and prefix the input specifies
16181762
expect(policy1.PolicyDocument.Statement[1].Resource)
16191763
.to.be.deep.equal('*');
16201764
});

0 commit comments

Comments
 (0)