serverless-iam-roles-per-function
Advanced tools
Comparing version 0.1.2 to 0.1.3
@@ -5,2 +5,13 @@ # Change Log | ||
<a name="0.1.3"></a> | ||
## [0.1.3](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.2...v0.1.3) (2018-02-20) | ||
### Features | ||
* new configuration to control default inherit or override behaviour ([542175f](https://github.com/functionalone/serverless-iam-roles-per-function/commit/542175f)) | ||
* support custom role names via the property: iamRoleStatementsName ([93cd015](https://github.com/functionalone/serverless-iam-roles-per-function/commit/93cd015)), closes [#2](https://github.com/functionalone/serverless-iam-roles-per-function/issues/2) | ||
<a name="0.1.2"></a> | ||
@@ -7,0 +18,0 @@ ## [0.1.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.1...v0.1.2) (2018-02-07) |
@@ -8,2 +8,3 @@ declare class ServerlessIamPerFunctionPlugin { | ||
awsPackagePlugin: any; | ||
defaultInherit: boolean; | ||
/** | ||
@@ -10,0 +11,0 @@ * |
"use strict"; | ||
const _ = require("lodash"); | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
} | ||
const lodash_1 = __importDefault(require("lodash")); | ||
class ServerlessIamPerFunctionPlugin { | ||
@@ -15,2 +18,3 @@ /** | ||
}; | ||
this.defaultInherit = lodash_1.default.get(this.serverless.service, "custom.serverless-iam-roles-per-function.defaultInherit", false); | ||
} | ||
@@ -36,6 +40,15 @@ validateStatements(statements) { | ||
const fnJoin = roleName['Fn::Join']; | ||
if (!_.isArray(fnJoin) || fnJoin.length !== 2 || !_.isArray(fnJoin[1]) || fnJoin[1].length < 2) { | ||
if (!lodash_1.default.isArray(fnJoin) || fnJoin.length !== 2 || !lodash_1.default.isArray(fnJoin[1]) || fnJoin[1].length < 2) { | ||
throw new this.serverless.classes.Error("Global Role Name is not in exepcted format. Got name: " + JSON.stringify(roleName)); | ||
} | ||
fnJoin[1].splice(2, 0, functionName); | ||
let length = 0; //calculate the expected length. Sum the lenght of each part | ||
for (const part of fnJoin[1]) { | ||
length += part.length; | ||
} | ||
length += (fnJoin[1].length - 1); //take into account the dashes between parts | ||
if (length > 64) { | ||
throw new this.serverless.classes.Error(`auto generated role name for function: ${functionName} is too long (over 64 chars). | ||
Try setting a custom role name using the property: iamRoleStatementsName.`); | ||
} | ||
return roleName; | ||
@@ -46,4 +59,4 @@ } | ||
const functionResource = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[functionResourceName]; | ||
if (_.isEmpty(functionResource) || _.isEmpty(functionResource.Properties) || _.isEmpty(functionResource.Properties.Role) || | ||
!_.isArray(functionResource.Properties.Role["Fn::GetAtt"]) || !_.isArray(functionResource.DependsOn)) { | ||
if (lodash_1.default.isEmpty(functionResource) || lodash_1.default.isEmpty(functionResource.Properties) || lodash_1.default.isEmpty(functionResource.Properties.Role) || | ||
!lodash_1.default.isArray(functionResource.Properties.Role["Fn::GetAtt"]) || !lodash_1.default.isArray(functionResource.DependsOn)) { | ||
throw new this.serverless.classes.Error("Function Resource is not in exepcted format. For function name: " + functionName); | ||
@@ -60,3 +73,3 @@ } | ||
const functionObject = this.serverless.service.getFunction(functionName); | ||
if (_.isEmpty(functionObject.iamRoleStatements)) { | ||
if (lodash_1.default.isEmpty(functionObject.iamRoleStatements)) { | ||
return; | ||
@@ -71,3 +84,3 @@ } | ||
const globalIamRole = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[globalRoleName]; | ||
const functionIamRole = _.cloneDeep(globalIamRole); | ||
const functionIamRole = lodash_1.default.cloneDeep(globalIamRole); | ||
//remove the statements | ||
@@ -88,3 +101,3 @@ const policyStatements = []; | ||
//set vpc if needed | ||
if (!_.isEmpty(functionObject.vpc) || !_.isEmpty(this.serverless.service.provider.vpc)) { | ||
if (!lodash_1.default.isEmpty(functionObject.vpc) || !lodash_1.default.isEmpty(this.serverless.service.provider.vpc)) { | ||
functionIamRole.Properties.ManagedPolicyArns = [ | ||
@@ -94,3 +107,4 @@ 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', | ||
} | ||
if (functionObject.iamRoleStatementsInherit && !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { | ||
if ((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
&& !lodash_1.default.isEmpty(this.serverless.service.provider.iamRoleStatements)) { | ||
for (const s of this.serverless.service.provider.iamRoleStatements) { | ||
@@ -104,3 +118,3 @@ policyStatements.push(s); | ||
} | ||
functionIamRole.Properties.RoleName = this.getFunctionRoleName(functionName); | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) + globalRoleName; | ||
@@ -112,3 +126,3 @@ this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; | ||
const allFunctions = this.serverless.service.getAllFunctions(); | ||
if (_.isEmpty(allFunctions)) { | ||
if (lodash_1.default.isEmpty(allFunctions)) { | ||
return; | ||
@@ -115,0 +129,0 @@ } |
{ | ||
"name": "serverless-iam-roles-per-function", | ||
"private": false, | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"engines": { | ||
@@ -35,3 +35,3 @@ "node": ">=6.10.0" | ||
"dependencies": { | ||
"lodash": "^4.17.4" | ||
"lodash": "^4.17.5" | ||
}, | ||
@@ -44,3 +44,3 @@ "devDependencies": { | ||
"chai": "^4.1.2", | ||
"mocha": "^5.0.0", | ||
"mocha": "^5.0.1", | ||
"rimraf": "^2.6.2", | ||
@@ -50,3 +50,3 @@ "serverless": "^1.26.0", | ||
"tslint": "^5.6.0", | ||
"typescript": "^2.6.1" | ||
"typescript": "^2.7.2" | ||
}, | ||
@@ -53,0 +53,0 @@ "files": [ |
@@ -28,2 +28,3 @@ # Serverless IAM Roles Per Function Plugin | ||
handler: handler.get | ||
iamRoleStatementsName: my-custom-role-name #optional custom role name setting instead of the default generated one | ||
iamRoleStatements: | ||
@@ -36,3 +37,3 @@ - Effect: "Allow" | ||
func2: | ||
handler: handler.put | ||
handler: handler.put | ||
iamRoleStatements: | ||
@@ -48,3 +49,3 @@ - Effect: "Allow" | ||
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`: | ||
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`: | ||
@@ -73,3 +74,16 @@ ```yaml | ||
If you wish to change the default behaviour to `inherit` instead of `override` it is possible to specify the following custom configuration: | ||
```yaml | ||
custom: | ||
serverless-iam-roles-per-function: | ||
defaultInherit: true | ||
``` | ||
## More Info | ||
**Introduction post**: | ||
[Serverless Framework: Defining Per-Function IAM Roles](https://medium.com/@glicht/serverless-framework-defining-per-function-iam-roles-c678fa09f46d) | ||
**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. | ||
@@ -76,0 +90,0 @@ |
@@ -1,2 +0,2 @@ | ||
import * as _ from 'lodash'; | ||
import _ from 'lodash'; | ||
@@ -9,2 +9,3 @@ class ServerlessIamPerFunctionPlugin { | ||
awsPackagePlugin: any; | ||
defaultInherit: boolean; | ||
@@ -22,2 +23,3 @@ /** | ||
}; | ||
this.defaultInherit = _.get(this.serverless.service, "custom.serverless-iam-roles-per-function.defaultInherit", false); | ||
} | ||
@@ -49,2 +51,11 @@ | ||
fnJoin[1].splice(2, 0, functionName); | ||
let length=0; //calculate the expected length. Sum the lenght of each part | ||
for (const part of fnJoin[1]) { | ||
length += part.length; | ||
} | ||
length += (fnJoin[1].length - 1); //take into account the dashes between parts | ||
if(length > 64) { //aws limits to 64 chars the role name | ||
throw new this.serverless.classes.Error(`auto generated role name for function: ${functionName} is too long (over 64 chars). | ||
Try setting a custom role name using the property: iamRoleStatementsName.`); | ||
} | ||
return roleName; | ||
@@ -101,3 +112,4 @@ } | ||
} | ||
if(functionObject.iamRoleStatementsInherit && !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { //add global statements | ||
if((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
&& !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { //add global statements | ||
for (const s of this.serverless.service.provider.iamRoleStatements) { | ||
@@ -111,3 +123,3 @@ policyStatements.push(s); | ||
} | ||
functionIamRole.Properties.RoleName = this.getFunctionRoleName(functionName); | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) + globalRoleName; | ||
@@ -114,0 +126,0 @@ this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; |
@@ -6,77 +6,132 @@ // tslint:disable:no-var-requires | ||
const funcWithIamTemplate = require('../../src/test/funcs-with-iam.json'); | ||
import _ from 'lodash'; | ||
describe('plugin tests', () => { | ||
let serverless: any; | ||
let plugin: any; | ||
function assertFunctionRoleName(name: string, roleNameObj: any) { | ||
assert.isArray(roleNameObj['Fn::Join']); | ||
assert.isTrue(roleNameObj['Fn::Join'][1].indexOf(name) >= 0, 'role name contains function name'); | ||
} | ||
beforeEach(() => { | ||
serverless = new Serverless(); | ||
Object.assign(serverless.service, funcWithIamTemplate); | ||
serverless.pluginManager.loadAllPlugins(); | ||
plugin = new Plugin(serverless); | ||
}); | ||
describe('defaultInherit not set', () => { | ||
let serverless: any; | ||
let plugin: any; | ||
describe('#constructor()', () => { | ||
it('should initialize the plugin', () => { | ||
assert.instanceOf(plugin, Plugin); | ||
}); | ||
}); | ||
beforeEach(() => { | ||
serverless = new Serverless(); | ||
Object.assign(serverless.service, funcWithIamTemplate); | ||
serverless.pluginManager.loadAllPlugins(); | ||
plugin = new Plugin(serverless); | ||
}); | ||
describe('#validateStatements', () => { | ||
it('should validate valid statement', () => { | ||
const statements = [{ | ||
Effect: "Allow", | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
}]; | ||
assert.doesNotThrow(() => {plugin.validateStatements(statements);}); | ||
describe('#constructor()', () => { | ||
it('should initialize the plugin', () => { | ||
assert.instanceOf(plugin, Plugin); | ||
}); | ||
it('defaultInherit shuuld be false', () => { | ||
assert.isFalse(plugin.defaultInherit); | ||
}); | ||
}); | ||
it('should throw an error for invalid statement', () => { | ||
const statements = [{ //missing effect | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
}]; | ||
assert.throws(() => {plugin.validateStatements(statements);}); | ||
describe('#validateStatements', () => { | ||
it('should validate valid statement', () => { | ||
const statements = [{ | ||
Effect: "Allow", | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
}]; | ||
assert.doesNotThrow(() => {plugin.validateStatements(statements);}); | ||
}); | ||
it('should throw an error for invalid statement', () => { | ||
const statements = [{ //missing effect | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
}]; | ||
assert.throws(() => {plugin.validateStatements(statements);}); | ||
}); | ||
}); | ||
}); | ||
describe('#getFunctionRoleName', () => { | ||
it('should return a name with the function name', () => { | ||
const name = 'test-name'; | ||
const roleName = plugin.getFunctionRoleName(name); | ||
assertFunctionRoleName(name, roleName); | ||
}); | ||
function assertFunctionRoleName(name: string, roleNameObj: any) { | ||
assert.isArray(roleNameObj['Fn::Join']); | ||
assert.isTrue(roleNameObj['Fn::Join'][1].indexOf(name) >= 0, 'role name contains function name'); | ||
} | ||
it('should throw an error on long name', () => { | ||
assert.throws(() => {plugin.getFunctionRoleName('long-long-long-long-long-long-long-long-long-long-long-name');}); | ||
}); | ||
}); | ||
describe('#getFunctionRoleName', () => { | ||
it('should return a name with the function name', () => { | ||
const name = 'test-name'; | ||
const roleName = plugin.getFunctionRoleName(name); | ||
assertFunctionRoleName(name, roleName); | ||
describe('#createRolesPerFunction', () => { | ||
it('should create role per function', () => { | ||
plugin.createRolesPerFunction(); | ||
const helloRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloIamRoleLambdaExecution; | ||
assert.isNotEmpty(helloRole); | ||
assertFunctionRoleName('hello', helloRole.Properties.RoleName); | ||
//check depends and role is set properlly | ||
const helloFunctionResource = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloLambdaFunction; | ||
assert.isTrue(helloFunctionResource.DependsOn.indexOf('HelloIamRoleLambdaExecution') >= 0, 'function resource depends on role'); | ||
assert.equal(helloFunctionResource.Properties.Role["Fn::GetAtt"][0], 'HelloIamRoleLambdaExecution', "function resource role is set properly"); | ||
const helloInheritRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloInheritIamRoleLambdaExecution; | ||
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); | ||
const 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'); | ||
}); | ||
}); | ||
}); | ||
describe('#createRolesPerFunction', () => { | ||
it('should create role per function', () => { | ||
plugin.createRolesPerFunction(); | ||
const helloRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloIamRoleLambdaExecution; | ||
assert.isNotEmpty(helloRole); | ||
assertFunctionRoleName('hello', helloRole.Properties.RoleName); | ||
//check depends and role is set properlly | ||
const helloFunctionResource = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloLambdaFunction; | ||
assert.isTrue(helloFunctionResource.DependsOn.indexOf('HelloIamRoleLambdaExecution') >= 0, 'function resource depends on role'); | ||
assert.equal(helloFunctionResource.Properties.Role["Fn::GetAtt"][0], 'HelloIamRoleLambdaExecution', "function resource role is set properly"); | ||
const helloInheritRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloInheritIamRoleLambdaExecution; | ||
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); | ||
const 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'); | ||
describe('defaultInherit set', () => { | ||
let serverless: any; | ||
let plugin: any; | ||
beforeEach(() => { | ||
serverless = new Serverless(); | ||
const funcWithIamTemplateMod = _.set(_.cloneDeep(funcWithIamTemplate), "custom.serverless-iam-roles-per-function.defaultInherit", true); | ||
//change helloInherit to false for testing | ||
funcWithIamTemplateMod.functions.helloInherit.iamRoleStatementsInherit = false; | ||
Object.assign(serverless.service, funcWithIamTemplateMod); | ||
serverless.pluginManager.loadAllPlugins(); | ||
plugin = new Plugin(serverless); | ||
}); | ||
describe('#constructor()', () => { | ||
it('defaultInherit shuuld be true', () => { | ||
assert.isTrue(plugin.defaultInherit); | ||
}); | ||
}); | ||
describe('#createRolesPerFunction', () => { | ||
it('should create role per function', () => { | ||
plugin.createRolesPerFunction(); | ||
const helloRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloIamRoleLambdaExecution; | ||
assert.isNotEmpty(helloRole); | ||
assertFunctionRoleName('hello', helloRole.Properties.RoleName); | ||
//check depends and role is set properlly | ||
const helloFunctionResource = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloLambdaFunction; | ||
assert.isTrue(helloFunctionResource.DependsOn.indexOf('HelloIamRoleLambdaExecution') >= 0, 'function resource depends on role'); | ||
assert.equal(helloFunctionResource.Properties.Role["Fn::GetAtt"][0], 'HelloIamRoleLambdaExecution', "function resource role is set properly"); | ||
let statements: any[] = helloRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject(statements.find((s) => s.Action[0] === "xray:PutTelemetryRecords"), 'global statements imported as defaultInherit is set'); | ||
assert.isObject(statements.find((s) => s.Action[0] === "dynamodb:GetItem"), 'per function statements imported upon inherit'); | ||
const helloInheritRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloInheritIamRoleLambdaExecution; | ||
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); | ||
statements = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject(statements.find((s) => s.Action[0] === "dynamodb:GetItem"), 'per function statements imported'); | ||
assert.isTrue(statements.find((s) => s.Action[0] === "xray:PutTelemetryRecords") === undefined, | ||
'global statements not imported as iamRoleStatementsInherit is false'); | ||
}); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
40394
680
90
Updatedlodash@^4.17.5