Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split IAM Policy from IAM Role & Improve DependsOn for Streams #4427

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/plugins/aws/lib/naming.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -97,8 +97,15 @@ module.exports = {
getRoleLogicalId() { getRoleLogicalId() {
return 'IamRoleLambdaExecution'; return 'IamRoleLambdaExecution';
}, },

// Policy // Policy
getPolicyLogicalId() {
return 'IamRoleLambdaExecutionPolicy';
},
getCustomPolicyName() {
const policyName = this.getPolicyName();
policyName['Fn::Join'][1].push('custom');
return policyName;
},
getPolicyName() { getPolicyName() {
return { // TODO should probably have name ordered and altered as above - see AWS docs return { // TODO should probably have name ordered and altered as above - see AWS docs
'Fn::Join': [ 'Fn::Join': [
Expand Down
8 changes: 3 additions & 5 deletions lib/plugins/aws/package/compile/events/stream/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class AwsCompileStreamEvents {
.getStreamLogicalId(functionName, streamType, streamName); .getStreamLogicalId(functionName, streamType, streamName);


const funcRole = functionObj.role || this.serverless.service.provider.role; const funcRole = functionObj.role || this.serverless.service.provider.role;
let dependsOn = '"IamRoleLambdaExecution"'; let dependsOn = `"${this.provider.naming.getPolicyLogicalId()}"`;
if (funcRole) { if (funcRole) {
if ( // check whether the custom role is an ARN if ( // check whether the custom role is an ARN
typeof funcRole === 'string' && typeof funcRole === 'string' &&
Expand Down Expand Up @@ -201,12 +201,10 @@ class AwsCompileStreamEvents {


// update the PolicyDocument statements (if default policy is used) // update the PolicyDocument statements (if default policy is used)
if (this.serverless.service.provider.compiledCloudFormationTemplate if (this.serverless.service.provider.compiledCloudFormationTemplate
.Resources.IamRoleLambdaExecution) { .Resources[this.provider.naming.getRoleLogicalId()]) {
const statement = this.serverless.service.provider.compiledCloudFormationTemplate const statement = this.serverless.service.provider.compiledCloudFormationTemplate
.Resources .Resources[this.provider.naming.getPolicyLogicalId()]
.IamRoleLambdaExecution
.Properties .Properties
.Policies[0]
.PolicyDocument .PolicyDocument
.Statement; .Statement;
if (dynamodbStreamStatement.Resource.length) { if (dynamodbStreamStatement.Resource.length) {
Expand Down
31 changes: 19 additions & 12 deletions lib/plugins/aws/package/compile/events/stream/index.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ describe('AwsCompileStreamEvents', () => {
], ],
}, },
}, },
IamRoleLambdaExecutionPolicy: {
Properties: {
PolicyDocument: {
Statement: [],
},
},
},
}, },
}; };
serverless.setProvider('aws', new AwsProvider(serverless)); serverless.setProvider('aws', new AwsProvider(serverless));
Expand Down Expand Up @@ -331,7 +338,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbFoo
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -366,7 +373,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBar
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -395,7 +402,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingDynamodbBaz
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -464,8 +471,8 @@ describe('AwsCompileStreamEvents', () => {
{ 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'] } { 'Fn::GetAtt': ['SomeDdbTable', 'StreamArn'] }
); );
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution .provider.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecutionPolicy
.Properties.Policies[0].PolicyDocument.Statement[0] .Properties.PolicyDocument.Statement[0]
).to.deep.equal( ).to.deep.equal(
{ {
Action: [ Action: [
Expand Down Expand Up @@ -588,7 +595,7 @@ describe('AwsCompileStreamEvents', () => {


expect(awsCompileStreamEvents.serverless.service.provider expect(awsCompileStreamEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources .compiledCloudFormationTemplate.Resources
.IamRoleLambdaExecution.Properties.Policies[0] .IamRoleLambdaExecutionPolicy.Properties
.PolicyDocument.Statement .PolicyDocument.Statement
).to.deep.equal(iamRoleStatements); ).to.deep.equal(iamRoleStatements);
}); });
Expand Down Expand Up @@ -629,7 +636,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisFoo
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -664,7 +671,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBar
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -693,7 +700,7 @@ describe('AwsCompileStreamEvents', () => {
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
.DependsOn .DependsOn
).to.equal('IamRoleLambdaExecution'); ).to.equal('IamRoleLambdaExecutionPolicy');
expect(awsCompileStreamEvents.serverless.service expect(awsCompileStreamEvents.serverless.service
.provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz .provider.compiledCloudFormationTemplate.Resources.FirstEventSourceMappingKinesisBaz
.Properties.EventSourceArn .Properties.EventSourceArn
Expand Down Expand Up @@ -749,7 +756,7 @@ describe('AwsCompileStreamEvents', () => {


expect(awsCompileStreamEvents.serverless.service.provider expect(awsCompileStreamEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources .compiledCloudFormationTemplate.Resources
.IamRoleLambdaExecution.Properties.Policies[0] .IamRoleLambdaExecutionPolicy.Properties
.PolicyDocument.Statement .PolicyDocument.Statement
).to.deep.equal(iamRoleStatements); ).to.deep.equal(iamRoleStatements);
}); });
Expand All @@ -764,11 +771,11 @@ describe('AwsCompileStreamEvents', () => {


awsCompileStreamEvents.compileStreamEvents(); awsCompileStreamEvents.compileStreamEvents();


// should be 1 because we've mocked the IamRoleLambdaExecution above // should be 2 because we've mocked IAM Role and Policy above
expect( expect(
Object.keys(awsCompileStreamEvents.serverless.service.provider Object.keys(awsCompileStreamEvents.serverless.service.provider
.compiledCloudFormationTemplate.Resources).length .compiledCloudFormationTemplate.Resources).length
).to.equal(1); ).to.equal(2);
}); });


it('should not add the IAM role statements when stream events are not given', () => { it('should not add the IAM role statements when stream events are not given', () => {
Expand Down
12 changes: 6 additions & 6 deletions lib/plugins/aws/package/compile/functions/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ class AwsCompileFunctions {
if (role.startsWith('arn:aws')) { if (role.startsWith('arn:aws')) {
// role is a statically definied iam arn // role is a statically definied iam arn
compiledFunction.Properties.Role = role; compiledFunction.Properties.Role = role;
} else if (role === 'IamRoleLambdaExecution') { } else if (role === this.provider.naming.getRoleLogicalId()) {
// role is the default role generated by the framework // role is the default role generated by the framework
compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] }; compiledFunction.Properties.Role = { 'Fn::GetAtt': [role, 'Arn'] };
compiledFunction.DependsOn = [ compiledFunction.DependsOn = [
'IamRoleLambdaExecution', this.provider.naming.getRoleLogicalId(),
]; ];
} else { } else {
// role is a Logical Role Name // role is a Logical Role Name
Expand Down Expand Up @@ -152,7 +152,7 @@ class AwsCompileFunctions {
if (splittedArn[0] === 'arn' && (splittedArn[2] === 'sns' || splittedArn[2] === 'sqs')) { if (splittedArn[0] === 'arn' && (splittedArn[2] === 'sns' || splittedArn[2] === 'sqs')) {
const dlqType = splittedArn[2]; const dlqType = splittedArn[2];
const iamRoleLambdaExecution = this.serverless.service.provider const iamRoleLambdaExecution = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution; .compiledCloudFormationTemplate.Resources[this.provider.naming.getRoleLogicalId()];
let stmt; let stmt;


newFunction.Properties.DeadLetterConfig = { newFunction.Properties.DeadLetterConfig = {
Expand All @@ -176,7 +176,7 @@ class AwsCompileFunctions {
return BbPromise.reject(new this.serverless.classes.Error(errorMessage)); return BbPromise.reject(new this.serverless.classes.Error(errorMessage));
} }


// update the PolicyDocument statements (if default policy is used) // update the inline PolicyDocument statements (if default policy is used)
if (iamRoleLambdaExecution) { if (iamRoleLambdaExecution) {
iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(stmt); iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push(stmt);
} }
Expand Down Expand Up @@ -212,7 +212,7 @@ class AwsCompileFunctions {
const splittedArn = arn.split(':'); const splittedArn = arn.split(':');
if (splittedArn[0] === 'arn' && (splittedArn[2] === 'kms')) { if (splittedArn[0] === 'arn' && (splittedArn[2] === 'kms')) {
const iamRoleLambdaExecution = this.serverless.service.provider const iamRoleLambdaExecution = this.serverless.service.provider
.compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution; .compiledCloudFormationTemplate.Resources[this.provider.naming.getRoleLogicalId()];


newFunction.Properties.KmsKeyArn = arn; newFunction.Properties.KmsKeyArn = arn;


Expand Down Expand Up @@ -323,7 +323,7 @@ class AwsCompileFunctions {
} else if ('role' in this.serverless.service.provider) { } else if ('role' in this.serverless.service.provider) {
this.compileRole(newFunction, this.serverless.service.provider.role); this.compileRole(newFunction, this.serverless.service.provider.role);
} else { } else {
this.compileRole(newFunction, 'IamRoleLambdaExecution'); this.compileRole(newFunction, this.provider.naming.getRoleLogicalId());
} }


if (!functionObject.vpc) functionObject.vpc = {}; if (!functionObject.vpc) functionObject.vpc = {};
Expand Down
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "[TO BE REPLACED]",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": []
},
"Roles": [{
"Ref": "[TO BE REPLACED]"
}]
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing EOL

Original file line number Original file line Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@
} }
] ]
} }
} }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should be reverted

36 changes: 23 additions & 13 deletions lib/plugins/aws/package/lib/mergeIamTemplates.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -75,14 +75,31 @@ module.exports = {
iamRoleLambdaExecutionTemplate.Properties.RoleName = this.provider.naming.getRoleName(); iamRoleLambdaExecutionTemplate.Properties.RoleName = this.provider.naming.getRoleName();
iamRoleLambdaExecutionTemplate.Properties.Policies[0] iamRoleLambdaExecutionTemplate.Properties.Policies[0]
.PolicyName = this.provider.naming.getPolicyName(); .PolicyName = this.provider.naming.getPolicyName();

_.merge( _.merge(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources, this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
{ {
[this.provider.naming.getRoleLogicalId()]: iamRoleLambdaExecutionTemplate, [this.provider.naming.getRoleLogicalId()]: iamRoleLambdaExecutionTemplate,
} }
); );


const iamPolicyTemplate = this.serverless.utils.readFileSync(
path.join(this.serverless.config.serverlessPath,
'plugins',
'aws',
'package',
'lib',
'iam-policy-lambda-execution-template.json')
);
iamPolicyTemplate.Properties.PolicyName = this.provider.naming.getCustomPolicyName();
iamPolicyTemplate.Properties.Roles[0].Ref = this.provider.naming.getRoleLogicalId();

_.merge(
this.serverless.service.provider.compiledCloudFormationTemplate.Resources,
{
[this.provider.naming.getPolicyLogicalId()]: iamPolicyTemplate,
}
);

this.serverless.service.getAllFunctions().forEach((functionName) => { this.serverless.service.getAllFunctions().forEach((functionName) => {
const functionObject = this.serverless.service.getFunction(functionName); const functionObject = this.serverless.service.getFunction(functionName);


Expand Down Expand Up @@ -112,18 +129,11 @@ module.exports = {
}); });


if (this.serverless.service.provider.iamRoleStatements) { if (this.serverless.service.provider.iamRoleStatements) {
// add custom iam role statements const iamPolicyDocument = this.serverless.service.provider.compiledCloudFormationTemplate
this.serverless.service.provider.compiledCloudFormationTemplate .Resources[this.provider.naming.getPolicyLogicalId()].Properties.PolicyDocument;
.Resources[this.provider.naming.getRoleLogicalId()] // add custom iam role statements to custom Policy
.Properties iamPolicyDocument.Statement = iamPolicyDocument.Statement
.Policies[0] .concat(this.serverless.service.provider.iamRoleStatements);
.PolicyDocument
.Statement = this.serverless.service.provider.compiledCloudFormationTemplate
.Resources[this.provider.naming.getRoleLogicalId()]
.Properties
.Policies[0]
.PolicyDocument
.Statement.concat(this.serverless.service.provider.iamRoleStatements);
} }


if (this.serverless.service.provider.iamManagedPolicies) { if (this.serverless.service.provider.iamManagedPolicies) {
Expand Down
5 changes: 2 additions & 3 deletions lib/plugins/aws/package/lib/mergeIamTemplates.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -148,11 +148,10 @@ describe('#mergeIamTemplates()', () => {
return awsPackage.mergeIamTemplates() return awsPackage.mergeIamTemplates()
.then(() => { .then(() => {
expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate expect(awsPackage.serverless.service.provider.compiledCloudFormationTemplate
.Resources[awsPackage.provider.naming.getRoleLogicalId()] .Resources[awsPackage.provider.naming.getPolicyLogicalId()]
.Properties .Properties
.Policies[0]
.PolicyDocument .PolicyDocument
.Statement[2] .Statement[0]
).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]); ).to.deep.equal(awsPackage.serverless.service.provider.iamRoleStatements[0]);
}); });
}); });
Expand Down
16 changes: 14 additions & 2 deletions lib/plugins/aws/package/lib/saveCompiledTemplate.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ module.exports = {
compiledTemplateFileName compiledTemplateFileName
); );


this.serverless.utils.writeFileSync(compiledTemplateFilePath, const compiledTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
this.serverless.service.provider.compiledCloudFormationTemplate); if (compiledTemplate.Resources[this.provider.naming.getPolicyLogicalId()]) {
const customIAMPolicyStatement = compiledTemplate
.Resources[this.provider.naming.getPolicyLogicalId()]
.Properties
.PolicyDocument
.Statement;
// remove custom IAM Policy if no custom statements (empty array would be invalid for CF)
if (!customIAMPolicyStatement.length) {
delete compiledTemplate.Resources[this.provider.naming.getPolicyLogicalId()];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ok to introduce further changes to compiledTemplate here? Maintance-wise it can be confusing.
So far it was purely about storing compiled template into file, now there's also a cleanup action added here.

Maybe we should rather introduce some dedicated cleanup/optimize step, prior save that one, and put it there to make it transparent?

}
}

this.serverless.utils.writeFileSync(compiledTemplateFilePath, compiledTemplate);


return BbPromise.resolve(); return BbPromise.resolve();
}, },
Expand Down
39 changes: 31 additions & 8 deletions lib/plugins/aws/package/lib/saveCompiledTemplate.test.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('#saveCompiledTemplate()', () => {
let awsPackage; let awsPackage;
let getCompiledTemplateFileNameStub; let getCompiledTemplateFileNameStub;
let writeFileSyncStub; let writeFileSyncStub;
let filePath;


beforeEach(() => { beforeEach(() => {
const options = {}; const options = {};
Expand All @@ -21,9 +22,16 @@ describe('#saveCompiledTemplate()', () => {
serverless.config.servicePath = 'my-service'; serverless.config.servicePath = 'my-service';
serverless.service = { serverless.service = {
provider: { provider: {
compiledCloudFormationTemplate: 'compiled content', compiledCloudFormationTemplate: {
Resources: {},
},
}, },
}; };
filePath = path.join(
awsPackage.serverless.config.servicePath,
'.serverless',
'compiled.json'
);
getCompiledTemplateFileNameStub = sinon getCompiledTemplateFileNameStub = sinon
.stub(awsPackage.provider.naming, 'getCompiledTemplateFileName') .stub(awsPackage.provider.naming, 'getCompiledTemplateFileName')
.returns('compiled.json'); .returns('compiled.json');
Expand All @@ -36,16 +44,31 @@ describe('#saveCompiledTemplate()', () => {
awsPackage.serverless.utils.writeFileSync.restore(); awsPackage.serverless.utils.writeFileSync.restore();
}); });


it('should write the compiled template to disk', () => { it('should write the compiled template to disk', () =>
const filePath = path.join( awsPackage.saveCompiledTemplate().then(() => {
awsPackage.serverless.config.servicePath, expect(getCompiledTemplateFileNameStub.calledOnce).to.equal(true);
'.serverless', const expectedInput = {
'compiled.json' Resources: {},
); };
expect(writeFileSyncStub.calledWithExactly(filePath, expectedInput)).to.equal(true);
})
);


it('should remove the custom IAM policy, if empty', () => {
serverless.service.provider.compiledCloudFormationTemplate.Resources
.IamRoleLambdaExecutionPolicy = {
Properties: {
PolicyDocument: {
Statement: [],
},
},
};
return awsPackage.saveCompiledTemplate().then(() => { return awsPackage.saveCompiledTemplate().then(() => {
expect(getCompiledTemplateFileNameStub.calledOnce).to.equal(true); expect(getCompiledTemplateFileNameStub.calledOnce).to.equal(true);
expect(writeFileSyncStub.calledWithExactly(filePath, 'compiled content')).to.equal(true); const expectedInput = {
Resources: {},
};
expect(writeFileSyncStub.calledWithExactly(filePath, expectedInput)).to.equal(true);
}); });
}); });
}); });