serverless-iam-roles-per-function
Advanced tools
Comparing version 0.1.3 to 0.1.4
@@ -5,2 +5,12 @@ # Change Log | ||
<a name="0.1.4"></a> | ||
## [0.1.4](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.3...v0.1.4) (2018-02-23) | ||
### Bug Fixes | ||
* support for stream based event sources (issue [#3](https://github.com/functionalone/serverless-iam-roles-per-function/issues/3)) ([3b63d49](https://github.com/functionalone/serverless-iam-roles-per-function/commit/3b63d49)) | ||
<a name="0.1.3"></a> | ||
@@ -7,0 +17,0 @@ ## [0.1.3](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.2...v0.1.3) (2018-02-20) |
@@ -17,10 +17,29 @@ declare class ServerlessIamPerFunctionPlugin { | ||
getFunctionRoleName(functionName: string): any; | ||
updateFunctionResourceRole(functionName: string, roleName: string, globalRoleName: string): void; | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @return the function resource name | ||
*/ | ||
updateFunctionResourceRole(functionName: string, roleName: string, globalRoleName: string): string; | ||
/** | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @return array of statements (possibly empty) | ||
*/ | ||
getStreamStatements(functionObject: any): any[]; | ||
/** | ||
* Will check if function has a definition of iamRoleStatements. If so will create a new Role for the function based on these statements. | ||
* @param functionName | ||
* @param functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
*/ | ||
createRoleForFunction(functionName: string): void; | ||
createRoleForFunction(functionName: string, functionToRoleMap: Map<string, string>): void; | ||
/** | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role then adjust the DependsOn | ||
* @param functionToRoleMap | ||
*/ | ||
setEventSourceMappings(functionToRoleMap: Map<string, string>): void; | ||
createRolesPerFunction(): void; | ||
} | ||
export = ServerlessIamPerFunctionPlugin; |
@@ -54,2 +54,9 @@ "use strict"; | ||
} | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @return the function resource name | ||
*/ | ||
updateFunctionResourceRole(functionName, roleName, globalRoleName) { | ||
@@ -64,8 +71,64 @@ const functionResourceName = this.serverless.providers.aws.naming.getLambdaLogicalId(functionName); | ||
functionResource.Properties.Role["Fn::GetAtt"][0] = roleName; | ||
return functionResourceName; | ||
} | ||
/** | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @return array of statements (possibly empty) | ||
*/ | ||
getStreamStatements(functionObject) { | ||
const res = []; | ||
if (!functionObject.events) { | ||
return res; | ||
} | ||
const dynamodbStreamStatement = { | ||
Effect: 'Allow', | ||
Action: [ | ||
'dynamodb:GetRecords', | ||
'dynamodb:GetShardIterator', | ||
'dynamodb:DescribeStream', | ||
'dynamodb:ListStreams', | ||
], | ||
Resource: [], | ||
}; | ||
const kinesisStreamStatement = { | ||
Effect: 'Allow', | ||
Action: [ | ||
'kinesis:GetRecords', | ||
'kinesis:GetShardIterator', | ||
'kinesis:DescribeStream', | ||
'kinesis:ListStreams', | ||
], | ||
Resource: [], | ||
}; | ||
for (const event of functionObject.events) { | ||
if (event.stream) { | ||
const streamArn = event.stream.arn || event.stream; | ||
const streamType = event.stream.type || streamArn.split(':')[2]; | ||
switch (streamType) { | ||
case 'dynamodb': | ||
dynamodbStreamStatement.Resource.push(streamArn); | ||
break; | ||
case 'kinesis': | ||
kinesisStreamStatement.Resource.push(streamArn); | ||
break; | ||
default: | ||
throw new this.serverless.classes.Error(`Unsupported stream type: ${streamType} for function: `, functionObject); | ||
} | ||
} | ||
} | ||
if (dynamodbStreamStatement.Resource.length) { | ||
res.push(dynamodbStreamStatement); | ||
} | ||
if (kinesisStreamStatement.Resource.length) { | ||
res.push(kinesisStreamStatement); | ||
} | ||
return res; | ||
} | ||
/** | ||
* Will check if function has a definition of iamRoleStatements. If so will create a new Role for the function based on these statements. | ||
* @param functionName | ||
* @param functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
*/ | ||
createRoleForFunction(functionName) { | ||
createRoleForFunction(functionName, functionToRoleMap) { | ||
const functionObject = this.serverless.service.getFunction(functionName); | ||
@@ -103,2 +166,5 @@ if (lodash_1.default.isEmpty(functionObject.iamRoleStatements)) { | ||
} | ||
for (const s of this.getStreamStatements(functionObject)) { | ||
policyStatements.push(s); | ||
} | ||
if ((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
@@ -117,4 +183,24 @@ && !lodash_1.default.isEmpty(this.serverless.service.provider.iamRoleStatements)) { | ||
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; | ||
this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
functionToRoleMap.set(functionResourceName, roleResourceName); | ||
} | ||
/** | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role then adjust the DependsOn | ||
* @param functionToRoleMap | ||
*/ | ||
setEventSourceMappings(functionToRoleMap) { | ||
for (const mapping of lodash_1.default.values(this.serverless.service.provider.compiledCloudFormationTemplate.Resources)) { | ||
if (mapping.Type && mapping.Type === 'AWS::Lambda::EventSourceMapping') { | ||
const functionNameFn = lodash_1.default.get(mapping, "Properties.FunctionName.Fn::GetAtt"); | ||
if (!lodash_1.default.isArray(functionNameFn)) { | ||
continue; | ||
} | ||
const functionName = functionNameFn[0]; | ||
const roleName = functionToRoleMap.get(functionName); | ||
if (roleName) { | ||
mapping.DependsOn = roleName; | ||
} | ||
} | ||
} | ||
} | ||
createRolesPerFunction() { | ||
@@ -125,5 +211,7 @@ const allFunctions = this.serverless.service.getAllFunctions(); | ||
} | ||
const functionToRoleMap = new Map(); | ||
for (const func of allFunctions) { | ||
this.createRoleForFunction(func); | ||
this.createRoleForFunction(func, functionToRoleMap); | ||
} | ||
this.setEventSourceMappings(functionToRoleMap); | ||
} | ||
@@ -130,0 +218,0 @@ } |
{ | ||
"name": "serverless-iam-roles-per-function", | ||
"private": false, | ||
"version": "0.1.3", | ||
"version": "0.1.4", | ||
"engines": { | ||
@@ -6,0 +6,0 @@ "node": ">=6.10.0" |
@@ -47,3 +47,3 @@ # Serverless IAM Roles Per Function Plugin | ||
By deafault, function level `iamRoleStatements` override the provider level definition. It is also possible to inherit the provider level definition by specifying the option `iamRoleStatementsInherit: true`: | ||
By default, function level `iamRoleStatements` override the provider level definition. It is also possible to inherit the provider level definition by specifying the option `iamRoleStatementsInherit: true`: | ||
@@ -72,3 +72,3 @@ ```yaml | ||
If you wish to change the default behaviour to `inherit` instead of `override` it is possible to specify the following custom configuration: | ||
If you wish to change the default behavior to `inherit` instead of `override` it is possible to specify the following custom configuration: | ||
@@ -87,3 +87,3 @@ ```yaml | ||
**Note**: Servless Framework provides support for defining custom IAM roles on a per function level through the use of the `role` property and creating CloudFormation resources, as documented [here](https://serverless.com/framework/docs/providers/aws/guide/iam#custom-iam-roles). This plugin doesn't support defining both the `role` property and `iamRoleStatements` at the function level. | ||
**Note**: Serverless Framework provides support for defining custom IAM roles on a per function level through the use of the `role` property and creating CloudFormation resources, as documented [here](https://serverless.com/framework/docs/providers/aws/guide/iam#custom-iam-roles). This plugin doesn't support defining both the `role` property and `iamRoleStatements` at the function level. | ||
@@ -93,2 +93,2 @@ [npm-image]:https://badge.fury.io/js/serverless-iam-roles-per-function.svg | ||
[sls-image]:http://public.serverless.com/badges/v3.svg | ||
[sls-url]:http://www.serverless.com | ||
[sls-url]:http://www.serverless.com |
import _ from 'lodash'; | ||
interface Statement { | ||
Effect: "Allow" | "Deny"; | ||
Action: string | string[]; | ||
Resource: string | any[]; | ||
} | ||
class ServerlessIamPerFunctionPlugin { | ||
@@ -61,3 +67,10 @@ | ||
updateFunctionResourceRole(functionName: string, roleName: string, globalRoleName: string) { | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @return the function resource name | ||
*/ | ||
updateFunctionResourceRole(functionName: string, roleName: string, globalRoleName: string): string { | ||
const functionResourceName = this.serverless.providers.aws.naming.getLambdaLogicalId(functionName); | ||
@@ -71,9 +84,66 @@ const functionResource = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[functionResourceName]; | ||
functionResource.Properties.Role["Fn::GetAtt"][0] = roleName; | ||
} | ||
return functionResourceName; | ||
} | ||
/** | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @return array of statements (possibly empty) | ||
*/ | ||
getStreamStatements(functionObject: any) { | ||
const res: any[] = []; | ||
if(!functionObject.events) { //no events | ||
return res; | ||
} | ||
const dynamodbStreamStatement: Statement = { | ||
Effect: 'Allow', | ||
Action: [ | ||
'dynamodb:GetRecords', | ||
'dynamodb:GetShardIterator', | ||
'dynamodb:DescribeStream', | ||
'dynamodb:ListStreams', | ||
], | ||
Resource: [], | ||
}; | ||
const kinesisStreamStatement: Statement = { | ||
Effect: 'Allow', | ||
Action: [ | ||
'kinesis:GetRecords', | ||
'kinesis:GetShardIterator', | ||
'kinesis:DescribeStream', | ||
'kinesis:ListStreams', | ||
], | ||
Resource: [], | ||
}; | ||
for (const event of functionObject.events) { | ||
if(event.stream) { | ||
const streamArn = event.stream.arn || event.stream; | ||
const streamType = event.stream.type || streamArn.split(':')[2]; | ||
switch (streamType) { | ||
case 'dynamodb': | ||
(dynamodbStreamStatement.Resource as any[]).push(streamArn); | ||
break; | ||
case 'kinesis': | ||
(kinesisStreamStatement.Resource as any[]).push(streamArn); | ||
break; | ||
default: | ||
throw new this.serverless.classes.Error(`Unsupported stream type: ${streamType} for function: `, functionObject); | ||
} | ||
} | ||
} | ||
if (dynamodbStreamStatement.Resource.length) { | ||
res.push(dynamodbStreamStatement); | ||
} | ||
if (kinesisStreamStatement.Resource.length) { | ||
res.push(kinesisStreamStatement); | ||
} | ||
return res; | ||
} | ||
/** | ||
* Will check if function has a definition of iamRoleStatements. If so will create a new Role for the function based on these statements. | ||
* @param functionName | ||
* @param functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
*/ | ||
createRoleForFunction(functionName: string) { | ||
createRoleForFunction(functionName: string, functionToRoleMap: Map<string, string>) { | ||
const functionObject = this.serverless.service.getFunction(functionName); | ||
@@ -92,3 +162,3 @@ if(_.isEmpty(functionObject.iamRoleStatements)) { | ||
//remove the statements | ||
const policyStatements: any[] = []; | ||
const policyStatements: Statement[] = []; | ||
functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; | ||
@@ -111,3 +181,6 @@ //set log statements | ||
]; | ||
} | ||
} | ||
for (const s of this.getStreamStatements(functionObject)) { //set stream statements (if needed) | ||
policyStatements.push(s); | ||
} | ||
if((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
@@ -126,5 +199,26 @@ && !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { //add global statements | ||
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; | ||
this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
functionToRoleMap.set(functionResourceName, roleResourceName); | ||
} | ||
/** | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role then adjust the DependsOn | ||
* @param functionToRoleMap | ||
*/ | ||
setEventSourceMappings(functionToRoleMap: Map<string, string>) { | ||
for (const mapping of _.values(this.serverless.service.provider.compiledCloudFormationTemplate.Resources)) { | ||
if(mapping.Type && mapping.Type === 'AWS::Lambda::EventSourceMapping') { | ||
const functionNameFn = _.get(mapping, "Properties.FunctionName.Fn::GetAtt"); | ||
if(!_.isArray(functionNameFn)) { | ||
continue; | ||
} | ||
const functionName = functionNameFn[0]; | ||
const roleName = functionToRoleMap.get(functionName); | ||
if(roleName) { | ||
mapping.DependsOn = roleName; | ||
} | ||
} | ||
} | ||
} | ||
createRolesPerFunction() { | ||
@@ -135,5 +229,7 @@ const allFunctions = this.serverless.service.getAllFunctions(); | ||
} | ||
const functionToRoleMap: Map<string, string> = new Map(); | ||
for (const func of allFunctions) { | ||
this.createRoleForFunction(func); | ||
} | ||
this.createRoleForFunction(func, functionToRoleMap); | ||
} | ||
this.setEventSourceMappings(functionToRoleMap); | ||
} | ||
@@ -140,0 +236,0 @@ } |
@@ -97,2 +97,14 @@ { | ||
"Action": [ | ||
"dynamodb:GetRecords", | ||
"dynamodb:GetShardIterator", | ||
"dynamodb:DescribeStream", | ||
"dynamodb:ListStreams" | ||
], | ||
"Resource": [ | ||
"arn:aws:dynamodb:us-east-1:123456789012:table/test/stream/2017-10-09T19:39:15.151" | ||
] | ||
}, | ||
{ | ||
"Effect": "Allow", | ||
"Action": [ | ||
"xray:PutTelemetryRecords", | ||
@@ -173,2 +185,28 @@ "xray:PutTraceSegments" | ||
}, | ||
"StreamHandlerLambdaFunction": { | ||
"Type": "AWS::Lambda::Function", | ||
"Properties": { | ||
"Code": { | ||
"S3Bucket": { | ||
"Ref": "ServerlessDeploymentBucket" | ||
}, | ||
"S3Key": "serverless/test-python/dev/1517233344526-2018-01-29T13:42:24.526Z/test-python.zip" | ||
}, | ||
"FunctionName": "test-python-dev-stream-handler", | ||
"Handler": "handler.stream", | ||
"MemorySize": 128, | ||
"Role": { | ||
"Fn::GetAtt": [ | ||
"IamRoleLambdaExecution", | ||
"Arn" | ||
] | ||
}, | ||
"Runtime": "python2.7", | ||
"Timeout": 15 | ||
}, | ||
"DependsOn": [ | ||
"StreamHandlerLogGroup", | ||
"IamRoleLambdaExecution" | ||
] | ||
}, | ||
"HelloLambdaVersionnJ9Yfm1HjU1A2HFWWfHbwg0iMvO1Ymsf3fPkNKFCIo": { | ||
@@ -183,4 +221,20 @@ "Type": "AWS::Lambda::Version", | ||
} | ||
}, | ||
"StreamHandlerEventSourceMappingDynamodbTest": { | ||
"Type": "AWS::Lambda::EventSourceMapping", | ||
"DependsOn": "IamRoleLambdaExecution", | ||
"Properties": { | ||
"BatchSize": 10, | ||
"EventSourceArn": "arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151", | ||
"FunctionName": { | ||
"Fn::GetAtt": [ | ||
"StreamHandlerLambdaFunction", | ||
"Arn" | ||
] | ||
}, | ||
"StartingPosition": "TRIM_HORIZON", | ||
"Enabled": "True" | ||
} | ||
} | ||
}, | ||
}, | ||
"Outputs": { | ||
@@ -259,2 +313,20 @@ "ServerlessDeploymentBucketName": { | ||
"vpc": {} | ||
}, | ||
"streamHandler": { | ||
"handler": "handler.stream", | ||
"iamRoleStatements": [ | ||
{ | ||
"Effect": "Allow", | ||
"Action": [ | ||
"dynamodb:GetItem" | ||
], | ||
"Resource": "arn:aws:dynamodb:us-east-1:*:table/test" | ||
} | ||
], | ||
"events": [ | ||
{"stream": "arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151"} | ||
], | ||
"name": "test-python-dev-stream-handler", | ||
"package": {}, | ||
"vpc": {} | ||
} | ||
@@ -261,0 +333,0 @@ }, |
// tslint:disable:no-var-requires | ||
import {assert} from 'chai'; | ||
const Plugin = require('../lib/index'); | ||
import Plugin from '../lib/index'; | ||
const Serverless = require('serverless/lib/Serverless'); | ||
@@ -85,5 +85,11 @@ const funcWithIamTemplate = require('../../src/test/funcs-with-iam.json'); | ||
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); | ||
const statements: any[] = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; | ||
let statements: any[] = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject(statements.find((s) => s.Action[0] === "xray:PutTelemetryRecords"), 'global statements imported upon inherit'); | ||
assert.isObject(statements.find((s) => s.Action[0] === "dynamodb:GetItem"), 'per function statements imported upon inherit'); | ||
const streamHandlerRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.StreamHandlerIamRoleLambdaExecution; | ||
assertFunctionRoleName('streamHandler', streamHandlerRole.Properties.RoleName); | ||
statements = streamHandlerRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject(statements.find((s) => s.Action[0] === "dynamodb:GetRecords"), 'stream statements included'); | ||
const streamMapping = serverless.service.provider.compiledCloudFormationTemplate.Resources.StreamHandlerEventSourceMappingDynamodbTest; | ||
assert.equal(streamMapping.DependsOn, "StreamHandlerIamRoleLambdaExecution"); | ||
}); | ||
@@ -90,0 +96,0 @@ }); |
Sorry, the diff of this file is not supported yet
53911
958
91