serverless-iam-roles-per-function
Advanced tools
Comparing version 3.0.0-d84bffd to 3.0.1-21342a9
@@ -5,3 +5,3 @@ # Changelog | ||
## [3.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.2...v3.0.0) (2020-11-02) | ||
### [3.0.1](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.0.0...v3.0.1) (2020-11-28) | ||
@@ -11,11 +11,15 @@ | ||
* Support for Serverless v2.5.0 ([#53](https://github.com/functionalone/serverless-iam-roles-per-function/issues/53)) ([09e56ae](https://github.com/functionalone/serverless-iam-roles-per-function/commit/09e56ae)) | ||
* Docs: added contributing section ([d9715ba](https://github.com/functionalone/serverless-iam-roles-per-function/commit/d9715ba)) | ||
## [3.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.2...v3.0.0) (2020-11-02) | ||
### Bug Fixes | ||
* Function properties schema validation fixe ([#63](https://github.com/functionalone/serverless-iam-roles-per-function/issues/63)) ([1f81264](https://github.com/functionalone/serverless-iam-roles-per-function/commit/1f81264)) | ||
### Features | ||
* Support for Serverless v2.5.0 ([#53](https://github.com/functionalone/serverless-iam-roles-per-function/issues/53)) ([09e56ae](https://github.com/functionalone/serverless-iam-roles-per-function/commit/09e56ae)) | ||
* nodejs 12 support ([#32](https://github.com/functionalone/serverless-iam-roles-per-function/issues/32)) ([4dd58a2](https://github.com/functionalone/serverless-iam-roles-per-function/commit/4dd58a2)) | ||
* Use resolved region name in counting length of role name ([#33](https://github.com/functionalone/serverless-iam-roles-per-function/issues/33)) ([f9fd677](https://github.com/functionalone/serverless-iam-roles-per-function/commit/f9fd677)), closes [#26](https://github.com/functionalone/serverless-iam-roles-per-function/issues/26) | ||
### [2.0.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.1...v2.0.2) (2019-08-30) | ||
## [2.0.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.1...v2.0.2) (2019-08-30) | ||
@@ -36,3 +40,3 @@ | ||
<a name="2.0.0"></a> | ||
# [2.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.4...v2.0.0) (2019-04-30) | ||
## [2.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.4...v2.0.0) (2019-04-30) | ||
@@ -88,3 +92,3 @@ | ||
<a name="1.0.0"></a> | ||
# [1.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.9...v1.0.0) (2018-05-29) | ||
## [1.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.9...v1.0.0) (2018-05-29) | ||
@@ -91,0 +95,0 @@ |
interface Statement { | ||
Effect: "Allow" | "Deny"; | ||
Effect: 'Allow' | 'Deny'; | ||
Action: string | string[]; | ||
@@ -7,3 +7,2 @@ Resource: string | any[]; | ||
declare class ServerlessIamPerFunctionPlugin { | ||
provider: string; | ||
hooks: { | ||
@@ -15,21 +14,36 @@ [i: string]: () => void; | ||
defaultInherit: boolean; | ||
readonly PROVIDER_AWS = "aws"; | ||
/** | ||
* | ||
* @param serverless - serverless host object | ||
* @param options | ||
* @param {Serverless} serverless - serverless host object | ||
* @param {Object} _options | ||
*/ | ||
constructor(serverless: any); | ||
constructor(serverless: any, _options?: any); | ||
/** | ||
* Utility function which throws an error. The msg will be formated with args using util.format. | ||
* Utility function which throws an error. The msg will be formatted with args using util.format. | ||
* Error message will be prefixed with ${PLUGIN_NAME}: ERROR: | ||
* @param {string} msg | ||
* @param {*[]} args | ||
* @returns void | ||
*/ | ||
throwError(msg: string, ...args: any[]): void; | ||
/** | ||
* @param {*} statements | ||
* @returns void | ||
*/ | ||
validateStatements(statements: any): void; | ||
getRoleNameLength(name_parts: any[]): number; | ||
/** | ||
* @param {*[]} nameParts | ||
* @returns void | ||
*/ | ||
getRoleNameLength(nameParts: any[]): number; | ||
/** | ||
* @param {string} functionName | ||
* @returns {string} | ||
*/ | ||
getFunctionRoleName(functionName: string): any; | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @param {string} functionName | ||
* @param {string} roleName | ||
* @param {string} globalRoleName | ||
* @return the function resource name | ||
@@ -40,3 +54,3 @@ */ | ||
* Get the necessary statement permissions if there are SQS event sources. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return statement (possibly null) | ||
@@ -47,3 +61,3 @@ */ | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return array of statements (possibly empty) | ||
@@ -53,14 +67,21 @@ */ | ||
/** | ||
* 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 | ||
* Will check if function has a definition of iamRoleStatements. | ||
* If so will create a new Role for the function based on these statements. | ||
* @param {string} functionName | ||
* @param {Map} functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
* @returns 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 | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role | ||
* then adjust the DependsOn | ||
* @param {Map} functionToRoleMap | ||
* @returns void | ||
*/ | ||
setEventSourceMappings(functionToRoleMap: Map<string, string>): void; | ||
/** | ||
* @returns void | ||
*/ | ||
createRolesPerFunction(): void; | ||
} | ||
export = ServerlessIamPerFunctionPlugin; |
@@ -11,8 +11,41 @@ "use strict"; | ||
* | ||
* @param serverless - serverless host object | ||
* @param options | ||
* @param {Serverless} serverless - serverless host object | ||
* @param {Object} _options | ||
*/ | ||
constructor(serverless) { | ||
this.provider = 'aws'; | ||
constructor(serverless, _options) { | ||
this.PROVIDER_AWS = 'aws'; | ||
this.serverless = serverless; | ||
if (this.serverless.service.provider.name !== this.PROVIDER_AWS) { | ||
throw new this.serverless.classes.Error(`${PLUGIN_NAME} plugin supports only AWS`); | ||
} | ||
// Added: Schema based validation of service config | ||
// https://github.com/serverless/serverless/releases/tag/v1.78.0 | ||
if (this.serverless.configSchemaHandler) { | ||
const newCustomPropSchema = { | ||
type: 'object', | ||
properties: { | ||
[PLUGIN_NAME]: { | ||
type: 'object', | ||
properties: { | ||
defaultInherit: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
}, | ||
}; | ||
serverless.configSchemaHandler.defineCustomProperties(newCustomPropSchema); | ||
// Added: defineFunctionProperties schema extension method | ||
// https://github.com/serverless/serverless/releases/tag/v2.10.0 | ||
if (this.serverless.configSchemaHandler.defineFunctionProperties) { | ||
this.serverless.configSchemaHandler.defineFunctionProperties(this.PROVIDER_AWS, { | ||
properties: { | ||
iamRoleStatementsInherit: { type: 'boolean' }, | ||
iamRoleStatementsName: { type: 'string' }, | ||
iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' }, | ||
}, | ||
}); | ||
} | ||
} | ||
this.hooks = { | ||
@@ -24,4 +57,7 @@ 'before:package:finalize': this.createRolesPerFunction.bind(this), | ||
/** | ||
* Utility function which throws an error. The msg will be formated with args using util.format. | ||
* Utility function which throws an error. The msg will be formatted with args using util.format. | ||
* Error message will be prefixed with ${PLUGIN_NAME}: ERROR: | ||
* @param {string} msg | ||
* @param {*[]} args | ||
* @returns void | ||
*/ | ||
@@ -32,8 +68,12 @@ throwError(msg, ...args) { | ||
} | ||
const err_msg = `${PLUGIN_NAME}: ERROR: ${msg}`; | ||
throw new this.serverless.classes.Error(err_msg); | ||
const errMsg = `${PLUGIN_NAME}: ERROR: ${msg}`; | ||
throw new this.serverless.classes.Error(errMsg); | ||
} | ||
/** | ||
* @param {*} statements | ||
* @returns void | ||
*/ | ||
validateStatements(statements) { | ||
// Verify that iamRoleStatements (if present) is an array of { Effect: ..., | ||
// Action: ..., Resource: ... } objects. | ||
// Verify that iamRoleStatements (if present) is an array of { Effect: ..., | ||
// Action: ..., Resource: ... } objects. | ||
if (lodash_1.default.isEmpty(statements)) { | ||
@@ -52,10 +92,8 @@ return; | ||
['Resource', 'NotResource'], | ||
].filter(props => props.every(prop => !statement[prop])); | ||
].filter((props) => props.every((prop) => !statement[prop])); | ||
return missing.length === 0 | ||
? null | ||
: `statement ${i} is missing the following properties: ${missing | ||
.map(m => m.join(' / ')) | ||
.join(', ')}`; | ||
: `statement ${i} is missing the following properties: ${missing.map((m) => m.join(' / ')).join(', ')}`; | ||
}); | ||
const flawed = descriptions.filter(curr => curr); | ||
const flawed = descriptions.filter((curr) => curr); | ||
if (flawed.length) { | ||
@@ -74,5 +112,9 @@ violationsFound = flawed.join('; '); | ||
} | ||
getRoleNameLength(name_parts) { | ||
let length = 0; //calculate the expected length. Sum the length of each part | ||
for (const part of name_parts) { | ||
/** | ||
* @param {*[]} nameParts | ||
* @returns void | ||
*/ | ||
getRoleNameLength(nameParts) { | ||
let length = 0; // calculate the expected length. Sum the length of each part | ||
for (const part of nameParts) { | ||
if (part.Ref) { | ||
@@ -90,5 +132,9 @@ if (part.Ref === 'AWS::Region') { | ||
} | ||
length += (name_parts.length - 1); //take into account the dashes between parts | ||
length += (nameParts.length - 1); // take into account the dashes between parts | ||
return length; | ||
} | ||
/** | ||
* @param {string} functionName | ||
* @returns {string} | ||
*/ | ||
getFunctionRoleName(functionName) { | ||
@@ -98,5 +144,5 @@ const roleName = this.serverless.providers.aws.naming.getRoleName(); | ||
if (!lodash_1.default.isArray(fnJoin) || fnJoin.length !== 2 || !lodash_1.default.isArray(fnJoin[1]) || fnJoin[1].length < 2) { | ||
this.throwError("Global Role Name is not in exepcted format. Got name: " + JSON.stringify(roleName)); | ||
this.throwError('Global Role Name is not in expected format. Got name: ' + JSON.stringify(roleName)); | ||
} | ||
fnJoin[1].splice(2, 0, functionName); //insert the function name | ||
fnJoin[1].splice(2, 0, functionName); // insert the function name | ||
if (this.getRoleNameLength(fnJoin[1]) > 64 && fnJoin[1][fnJoin[1].length - 1] === 'lambdaRole') { | ||
@@ -106,3 +152,3 @@ // Remove lambdaRole from name to give more space for function name. | ||
} | ||
if (this.getRoleNameLength(fnJoin[1]) > 64) { //aws limits to 64 chars the role name | ||
if (this.getRoleNameLength(fnJoin[1]) > 64) { // aws limits to 64 chars the role name | ||
this.throwError(`auto generated role name for function: ${functionName} is too long (over 64 chars). | ||
@@ -114,6 +160,5 @@ Try setting a custom role name using the property: iamRoleStatementsName.`); | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @param {string} functionName | ||
* @param {string} roleName | ||
* @param {string} globalRoleName | ||
* @return the function resource name | ||
@@ -123,9 +168,13 @@ */ | ||
const functionResourceName = this.serverless.providers.aws.naming.getLambdaLogicalId(functionName); | ||
const functionResource = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[functionResourceName]; | ||
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)) { | ||
this.throwError("Function Resource is not in exepcted format. For function name: " + functionName); | ||
const functionResource = this.serverless.service.provider | ||
.compiledCloudFormationTemplate.Resources[functionResourceName]; | ||
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)) { | ||
this.throwError('Function Resource is not in expected format. For function name: ' + functionName); | ||
} | ||
functionResource.DependsOn = [roleName].concat(functionResource.DependsOn.filter(((val) => val !== globalRoleName))); | ||
functionResource.Properties.Role["Fn::GetAtt"][0] = roleName; | ||
functionResource.Properties.Role['Fn::GetAtt'][0] = roleName; | ||
return functionResourceName; | ||
@@ -135,3 +184,3 @@ } | ||
* Get the necessary statement permissions if there are SQS event sources. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return statement (possibly null) | ||
@@ -159,3 +208,3 @@ */ | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return array of statements (possibly empty) | ||
@@ -165,3 +214,3 @@ */ | ||
const res = []; | ||
if (lodash_1.default.isEmpty(functionObject.events)) { //no events | ||
if (lodash_1.default.isEmpty(functionObject.events)) { // no events | ||
return res; | ||
@@ -214,5 +263,7 @@ } | ||
/** | ||
* 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 | ||
* Will check if function has a definition of iamRoleStatements. | ||
* If so will create a new Role for the function based on these statements. | ||
* @param {string} functionName | ||
* @param {Map} functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
* @returns void | ||
*/ | ||
@@ -225,16 +276,17 @@ createRoleForFunction(functionName, functionToRoleMap) { | ||
if (functionObject.role) { | ||
this.throwError("Defing function with both 'role' and 'iamRoleStatements' is not supported. Function name: " + functionName); | ||
this.throwError('Define function with both \'role\' and \'iamRoleStatements\' is not supported. Function name: ' | ||
+ functionName); | ||
} | ||
this.validateStatements(functionObject.iamRoleStatements); | ||
//we use the configured role as a template | ||
// we use the configured role as a template | ||
const globalRoleName = this.serverless.providers.aws.naming.getRoleLogicalId(); | ||
const globalIamRole = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[globalRoleName]; | ||
const functionIamRole = lodash_1.default.cloneDeep(globalIamRole); | ||
//remove the statements | ||
// remove the statements | ||
const policyStatements = []; | ||
functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; | ||
//set log statements | ||
// set log statements | ||
policyStatements[0] = { | ||
Effect: "Allow", | ||
Action: ["logs:CreateLogStream", "logs:PutLogEvents"], | ||
Effect: 'Allow', | ||
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'], | ||
Resource: [ | ||
@@ -249,3 +301,3 @@ { | ||
functionIamRole.Properties.ManagedPolicyArns = []; | ||
//set vpc if needed | ||
// set vpc if needed | ||
if (!lodash_1.default.isEmpty(functionObject.vpc) || !lodash_1.default.isEmpty(this.serverless.service.provider.vpc)) { | ||
@@ -262,6 +314,6 @@ functionIamRole.Properties.ManagedPolicyArns = [{ | ||
} | ||
for (const s of this.getStreamStatements(functionObject)) { //set stream statements (if needed) | ||
for (const s of this.getStreamStatements(functionObject)) { // set stream statements (if needed) | ||
policyStatements.push(s); | ||
} | ||
const sqsStatement = this.getSqsStatement(functionObject); //set sqs statement (if needed) | ||
const sqsStatement = this.getSqsStatement(functionObject); // set sqs statement (if needed) | ||
if (sqsStatement) { | ||
@@ -272,3 +324,3 @@ policyStatements.push(sqsStatement); | ||
// currently only sns is supported: https://serverless.com/framework/docs/providers/aws/events/sns#dlq-with-sqs | ||
if (!lodash_1.default.isEmpty(functionObject.onError)) { // | ||
if (!lodash_1.default.isEmpty(functionObject.onError)) { | ||
policyStatements.push({ | ||
@@ -282,4 +334,5 @@ Effect: 'Allow', | ||
} | ||
if ((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
&& !lodash_1.default.isEmpty(this.serverless.service.provider.iamRoleStatements)) { //add global statements | ||
const isInherit = functionObject.iamRoleStatementsInherit | ||
|| (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false); | ||
if (isInherit && !lodash_1.default.isEmpty(this.serverless.service.provider.iamRoleStatements)) { // add global statements | ||
for (const s of this.serverless.service.provider.iamRoleStatements) { | ||
@@ -289,3 +342,3 @@ policyStatements.push(s); | ||
} | ||
//add iamRoleStatements | ||
// add iamRoleStatements | ||
if (lodash_1.default.isArray(functionObject.iamRoleStatements)) { | ||
@@ -296,4 +349,6 @@ for (const s of functionObject.iamRoleStatements) { | ||
} | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) + globalRoleName; | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName | ||
|| this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) | ||
+ globalRoleName; | ||
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; | ||
@@ -304,4 +359,6 @@ const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
/** | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role then adjust the DependsOn | ||
* @param functionToRoleMap | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role | ||
* then adjust the DependsOn | ||
* @param {Map} functionToRoleMap | ||
* @returns void | ||
*/ | ||
@@ -311,3 +368,3 @@ setEventSourceMappings(functionToRoleMap) { | ||
if (mapping.Type && mapping.Type === 'AWS::Lambda::EventSourceMapping') { | ||
const functionNameFn = lodash_1.default.get(mapping, "Properties.FunctionName.Fn::GetAtt"); | ||
const functionNameFn = lodash_1.default.get(mapping, 'Properties.FunctionName.Fn::GetAtt'); | ||
if (!lodash_1.default.isArray(functionNameFn)) { | ||
@@ -324,2 +381,5 @@ continue; | ||
} | ||
/** | ||
* @returns void | ||
*/ | ||
createRolesPerFunction() { | ||
@@ -326,0 +386,0 @@ const allFunctions = this.serverless.service.getAllFunctions(); |
{ | ||
"name": "serverless-iam-roles-per-function", | ||
"private": false, | ||
"version": "3.0.0-d84bffd", | ||
"version": "3.0.1-21342a9", | ||
"engines": { | ||
@@ -18,3 +18,4 @@ "node": ">=10" | ||
"prepublishOnly": "npm run clean && npm run compile", | ||
"release": "standard-version" | ||
"release": "standard-version", | ||
"lint": "eslint ." | ||
}, | ||
@@ -40,22 +41,25 @@ "author": "Functional One, Ltd.", | ||
"dependencies": { | ||
"lodash": "^4.17.15" | ||
"lodash": "^4.17.20" | ||
}, | ||
"devDependencies": { | ||
"@serverless/enterprise-plugin": "^1.3.10", | ||
"@types/chai": "^4.2.0", | ||
"@types/lodash": "^4.14.138", | ||
"@types/mocha": "^5.2.7", | ||
"@types/node": "^6.14.7", | ||
"@serverless/enterprise-plugin": "^4.1.2", | ||
"@types/chai": "^4.2.14", | ||
"@types/lodash": "^4.14.165", | ||
"@types/mocha": "^8.0.4", | ||
"@types/node": "^12.19.6", | ||
"@typescript-eslint/eslint-plugin": "^4.8.2", | ||
"@typescript-eslint/parser": "^4.8.2", | ||
"chai": "^4.2.0", | ||
"coveralls": "^3.0.6", | ||
"mocha": "^6.2.0", | ||
"coveralls": "^3.1.0", | ||
"eslint": "^7.14.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"mocha": "^8.2.1", | ||
"npm-get-version": "^1.0.2", | ||
"nyc": "^14.1.1", | ||
"rimraf": "^3.0.0", | ||
"serverless": "^1.51.0", | ||
"source-map-support": "^0.5.13", | ||
"standard-version": "^8.0.1", | ||
"ts-node": "^8.3.0", | ||
"tslint": "^5.19.0", | ||
"typescript": "^3.6.2" | ||
"nyc": "^15.1.0", | ||
"rimraf": "^3.0.2", | ||
"serverless": "^2.12.0", | ||
"source-map-support": "^0.5.19", | ||
"standard-version": "^9.0.0", | ||
"ts-node": "^9.0.0", | ||
"typescript": "^4.1.2" | ||
}, | ||
@@ -87,5 +91,6 @@ "files": [ | ||
"skip": { | ||
"tag": true | ||
"tag": true, | ||
"commit": true | ||
} | ||
} | ||
} |
@@ -128,2 +128,20 @@ # Serverless IAM Roles Per Function Plugin | ||
## Contributing | ||
Contributions are welcome and appreciated. | ||
* Before opening a PR it is best to first open an [issue](https://github.com/functionalone/serverless-iam-roles-per-function/issues/new). Describe in the issue what you want you plan to implement/fix. Based on the feedback in the issue, you should be able to plan how to implement your PR. | ||
* Once ready, open a [PR](https://github.com/functionalone/serverless-iam-roles-per-function/compare) to contribute your code. | ||
* To help updating the [CHANGELOG.md](CHANGELOG.md) file, we use [standard-version](https://github.com/conventional-changelog/standard-version). Make sure to use conventional commit messages as specified at: https://www.conventionalcommits.org/en/v1.0.0/. | ||
* Update the release notes at [CHANGELOG.md](CHANGELOG.md) and bump the version by running: | ||
``` | ||
npm run release | ||
``` | ||
* Examine the [CHANGELOG.md](CHANGELOG.md) and update if still required. | ||
* Don't forget to commit the files modified by `npm run release` (we have the auto-commit option disabled by default). | ||
* Once the PR is approved and merged into master, travis-ci will automatically tag the version you created and deploy to npmjs under the `next` tag. You will see your version deployed at: https://www.npmjs.com/package/serverless-iam-roles-per-function?activeTab=versions. | ||
* Test your deployed version by installing with the `next` tag. For example: | ||
``` | ||
npm install --save-dev serverless-iam-roles-per-function@next | ||
``` | ||
## More Info | ||
@@ -130,0 +148,0 @@ |
@@ -7,3 +7,3 @@ import _ from 'lodash'; | ||
interface Statement { | ||
Effect: "Allow" | "Deny"; | ||
Effect: 'Allow' | 'Deny'; | ||
Action: string | string[]; | ||
@@ -15,3 +15,2 @@ Resource: string | any[]; | ||
provider: string; | ||
hooks: {[i: string]: () => void}; | ||
@@ -22,10 +21,48 @@ serverless: any; | ||
readonly PROVIDER_AWS = 'aws'; | ||
/** | ||
* | ||
* @param serverless - serverless host object | ||
* @param options | ||
* @param {Serverless} serverless - serverless host object | ||
* @param {Object} _options | ||
*/ | ||
constructor(serverless: any) { | ||
this.provider = 'aws'; | ||
constructor(serverless: any, _options?: any) { | ||
this.serverless = serverless; | ||
if (this.serverless.service.provider.name !== this.PROVIDER_AWS) { | ||
throw new this.serverless.classes.Error(`${PLUGIN_NAME} plugin supports only AWS`); | ||
} | ||
// Added: Schema based validation of service config | ||
// https://github.com/serverless/serverless/releases/tag/v1.78.0 | ||
if (this.serverless.configSchemaHandler) { | ||
const newCustomPropSchema = { | ||
type: 'object', | ||
properties: { | ||
[PLUGIN_NAME]: { | ||
type: 'object', | ||
properties: { | ||
defaultInherit: { | ||
type: 'boolean', | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
}, | ||
}; | ||
serverless.configSchemaHandler.defineCustomProperties(newCustomPropSchema); | ||
// Added: defineFunctionProperties schema extension method | ||
// https://github.com/serverless/serverless/releases/tag/v2.10.0 | ||
if (this.serverless.configSchemaHandler.defineFunctionProperties) { | ||
this.serverless.configSchemaHandler.defineFunctionProperties(this.PROVIDER_AWS, { | ||
properties: { | ||
iamRoleStatementsInherit: { type: 'boolean' }, | ||
iamRoleStatementsName: { type: 'string' }, | ||
iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' }, | ||
}, | ||
}); | ||
} | ||
} | ||
this.hooks = { | ||
@@ -38,54 +75,63 @@ 'before:package:finalize': this.createRolesPerFunction.bind(this), | ||
/** | ||
* Utility function which throws an error. The msg will be formated with args using util.format. | ||
* Utility function which throws an error. The msg will be formatted with args using util.format. | ||
* Error message will be prefixed with ${PLUGIN_NAME}: ERROR: | ||
* @param {string} msg | ||
* @param {*[]} args | ||
* @returns void | ||
*/ | ||
throwError(msg: string, ...args: any[]) { | ||
if(!_.isEmpty(args)) { | ||
if (!_.isEmpty(args)) { | ||
msg = util.format(msg, args); | ||
} | ||
const err_msg = `${PLUGIN_NAME}: ERROR: ${msg}`; | ||
throw new this.serverless.classes.Error(err_msg); | ||
const errMsg = `${PLUGIN_NAME}: ERROR: ${msg}`; | ||
throw new this.serverless.classes.Error(errMsg); | ||
} | ||
validateStatements(statements: any): void { | ||
// Verify that iamRoleStatements (if present) is an array of { Effect: ..., | ||
// Action: ..., Resource: ... } objects. | ||
if(_.isEmpty(statements)) { | ||
/** | ||
* @param {*} statements | ||
* @returns void | ||
*/ | ||
validateStatements(statements: any): void { | ||
// Verify that iamRoleStatements (if present) is an array of { Effect: ..., | ||
// Action: ..., Resource: ... } objects. | ||
if (_.isEmpty(statements)) { | ||
return; | ||
} | ||
let violationsFound; | ||
if (!Array.isArray(statements)) { | ||
violationsFound = 'it is not an array'; | ||
} else { | ||
const descriptions = statements.map((statement, i) => { | ||
const missing = [ | ||
['Effect'], | ||
['Action', 'NotAction'], | ||
['Resource', 'NotResource'], | ||
].filter(props => props.every(prop => !statement[prop])); | ||
return missing.length === 0 | ||
? null | ||
: `statement ${i} is missing the following properties: ${missing | ||
.map(m => m.join(' / ')) | ||
.join(', ')}`; | ||
}); | ||
const flawed = descriptions.filter(curr => curr); | ||
if (flawed.length) { | ||
violationsFound = flawed.join('; '); | ||
} | ||
} | ||
let violationsFound; | ||
if (!Array.isArray(statements)) { | ||
violationsFound = 'it is not an array'; | ||
} else { | ||
const descriptions = statements.map((statement, i) => { | ||
const missing = [ | ||
['Effect'], | ||
['Action', 'NotAction'], | ||
['Resource', 'NotResource'], | ||
].filter((props) => props.every((prop) => !statement[prop])); | ||
return missing.length === 0 | ||
? null | ||
: `statement ${i} is missing the following properties: ${missing.map((m) => m.join(' / ')).join(', ')}`; | ||
}); | ||
const flawed = descriptions.filter((curr) => curr); | ||
if (flawed.length) { | ||
violationsFound = flawed.join('; '); | ||
} | ||
} | ||
if (violationsFound) { | ||
const errorMessage = [ | ||
'iamRoleStatements should be an array of objects,', | ||
' where each object has Effect, Action / NotAction, Resource / NotResource fields.', | ||
` Specifically, ${violationsFound}`, | ||
].join(''); | ||
this.throwError(errorMessage); | ||
} | ||
if (violationsFound) { | ||
const errorMessage = [ | ||
'iamRoleStatements should be an array of objects,', | ||
' where each object has Effect, Action / NotAction, Resource / NotResource fields.', | ||
` Specifically, ${violationsFound}`, | ||
].join(''); | ||
this.throwError(errorMessage); | ||
} | ||
} | ||
getRoleNameLength(name_parts: any[]) { | ||
let length=0; //calculate the expected length. Sum the length of each part | ||
for (const part of name_parts) { | ||
/** | ||
* @param {*[]} nameParts | ||
* @returns void | ||
*/ | ||
getRoleNameLength(nameParts: any[]) { | ||
let length = 0; // calculate the expected length. Sum the length of each part | ||
for (const part of nameParts) { | ||
if (part.Ref) { | ||
@@ -101,18 +147,22 @@ if (part.Ref === 'AWS::Region') { | ||
} | ||
length += (name_parts.length - 1); //take into account the dashes between parts | ||
length += (nameParts.length - 1); // take into account the dashes between parts | ||
return length; | ||
} | ||
/** | ||
* @param {string} functionName | ||
* @returns {string} | ||
*/ | ||
getFunctionRoleName(functionName: string) { | ||
const roleName = this.serverless.providers.aws.naming.getRoleName(); | ||
const fnJoin = roleName['Fn::Join']; | ||
if(!_.isArray(fnJoin) || fnJoin.length !== 2 || !_.isArray(fnJoin[1]) || fnJoin[1].length < 2) { | ||
this.throwError("Global Role Name is not in exepcted format. Got name: " + JSON.stringify(roleName)); | ||
if (!_.isArray(fnJoin) || fnJoin.length !== 2 || !_.isArray(fnJoin[1]) || fnJoin[1].length < 2) { | ||
this.throwError('Global Role Name is not in expected format. Got name: ' + JSON.stringify(roleName)); | ||
} | ||
fnJoin[1].splice(2, 0, functionName); //insert the function name | ||
if(this.getRoleNameLength(fnJoin[1]) > 64 && fnJoin[1][fnJoin[1].length-1] === 'lambdaRole') { | ||
fnJoin[1].splice(2, 0, functionName); // insert the function name | ||
if (this.getRoleNameLength(fnJoin[1]) > 64 && fnJoin[1][fnJoin[1].length - 1] === 'lambdaRole') { | ||
// Remove lambdaRole from name to give more space for function name. | ||
fnJoin[1].pop(); | ||
} | ||
if(this.getRoleNameLength(fnJoin[1]) > 64) { //aws limits to 64 chars the role name | ||
if (this.getRoleNameLength(fnJoin[1]) > 64) { // aws limits to 64 chars the role name | ||
this.throwError(`auto generated role name for function: ${functionName} is too long (over 64 chars). | ||
@@ -125,6 +175,5 @@ Try setting a custom role name using the property: iamRoleStatementsName.`); | ||
/** | ||
* | ||
* @param functionName | ||
* @param roleName | ||
* @param globalRoleName | ||
* @param {string} functionName | ||
* @param {string} roleName | ||
* @param {string} globalRoleName | ||
* @return the function resource name | ||
@@ -134,9 +183,17 @@ */ | ||
const functionResourceName = this.serverless.providers.aws.naming.getLambdaLogicalId(functionName); | ||
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)) { | ||
this.throwError("Function Resource is not in exepcted format. For function name: " + functionName); | ||
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) | ||
) { | ||
this.throwError('Function Resource is not in expected format. For function name: ' + functionName); | ||
} | ||
functionResource.DependsOn = [roleName].concat(functionResource.DependsOn.filter(((val: any) => val !== globalRoleName ))); | ||
functionResource.Properties.Role["Fn::GetAtt"][0] = roleName; | ||
functionResource.DependsOn = [roleName].concat( | ||
functionResource.DependsOn.filter(((val: any) => val !== globalRoleName )), | ||
); | ||
functionResource.Properties.Role['Fn::GetAtt'][0] = roleName; | ||
return functionResourceName; | ||
@@ -147,3 +204,3 @@ } | ||
* Get the necessary statement permissions if there are SQS event sources. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return statement (possibly null) | ||
@@ -162,3 +219,3 @@ */ | ||
for (const event of functionObject.events) { | ||
if(event.sqs) { | ||
if (event.sqs) { | ||
const sqsArn = event.sqs.arn || event.sqs; | ||
@@ -173,3 +230,3 @@ (sqsStatement.Resource as any[]).push(sqsArn); | ||
* Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. | ||
* @param functionObject | ||
* @param {*} functionObject | ||
* @return array of statements (possibly empty) | ||
@@ -179,3 +236,3 @@ */ | ||
const res: any[] = []; | ||
if(_.isEmpty(functionObject.events)) { //no events | ||
if (_.isEmpty(functionObject.events)) { // no events | ||
return res; | ||
@@ -204,3 +261,3 @@ } | ||
for (const event of functionObject.events) { | ||
if(event.stream) { | ||
if (event.stream) { | ||
const streamArn = event.stream.arn || event.stream; | ||
@@ -230,26 +287,31 @@ const streamType = event.stream.type || streamArn.split(':')[2]; | ||
/** | ||
* 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 | ||
* Will check if function has a definition of iamRoleStatements. | ||
* If so will create a new Role for the function based on these statements. | ||
* @param {string} functionName | ||
* @param {Map} functionToRoleMap - populate the map with a mapping from function resource name to role resource name | ||
* @returns void | ||
*/ | ||
createRoleForFunction(functionName: string, functionToRoleMap: Map<string, string>) { | ||
const functionObject = this.serverless.service.getFunction(functionName); | ||
if(functionObject.iamRoleStatements === undefined) { | ||
if (functionObject.iamRoleStatements === undefined) { | ||
return; | ||
} | ||
if(functionObject.role) { | ||
this.throwError("Defing function with both 'role' and 'iamRoleStatements' is not supported. Function name: " + functionName); | ||
if (functionObject.role) { | ||
this.throwError( | ||
'Define function with both \'role\' and \'iamRoleStatements\' is not supported. Function name: ' | ||
+ functionName, | ||
); | ||
} | ||
this.validateStatements(functionObject.iamRoleStatements); | ||
//we use the configured role as a template | ||
// we use the configured role as a template | ||
const globalRoleName = this.serverless.providers.aws.naming.getRoleLogicalId(); | ||
const globalIamRole = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[globalRoleName]; | ||
const functionIamRole = _.cloneDeep(globalIamRole); | ||
//remove the statements | ||
// remove the statements | ||
const policyStatements: Statement[] = []; | ||
functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; | ||
//set log statements | ||
// set log statements | ||
policyStatements[0] = { | ||
Effect: "Allow", | ||
Action: ["logs:CreateLogStream", "logs:PutLogEvents"], | ||
Effect: 'Allow', | ||
Action: ['logs:CreateLogStream', 'logs:PutLogEvents'], | ||
Resource: [ | ||
@@ -264,3 +326,3 @@ { | ||
functionIamRole.Properties.ManagedPolicyArns = []; | ||
//set vpc if needed | ||
// set vpc if needed | ||
if (!_.isEmpty(functionObject.vpc) || !_.isEmpty(this.serverless.service.provider.vpc)) { | ||
@@ -277,6 +339,6 @@ functionIamRole.Properties.ManagedPolicyArns = [{ | ||
} | ||
for (const s of this.getStreamStatements(functionObject)) { //set stream statements (if needed) | ||
for (const s of this.getStreamStatements(functionObject)) { // set stream statements (if needed) | ||
policyStatements.push(s); | ||
} | ||
const sqsStatement = this.getSqsStatement(functionObject); //set sqs statement (if needed) | ||
const sqsStatement = this.getSqsStatement(functionObject); // set sqs statement (if needed) | ||
if (sqsStatement) { | ||
@@ -287,3 +349,3 @@ policyStatements.push(sqsStatement); | ||
// currently only sns is supported: https://serverless.com/framework/docs/providers/aws/events/sns#dlq-with-sqs | ||
if (!_.isEmpty(functionObject.onError)) { // | ||
if (!_.isEmpty(functionObject.onError)) { | ||
policyStatements.push({ | ||
@@ -297,4 +359,7 @@ Effect: 'Allow', | ||
} | ||
if((functionObject.iamRoleStatementsInherit || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false)) | ||
&& !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { //add global statements | ||
const isInherit = functionObject.iamRoleStatementsInherit | ||
|| (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false); | ||
if (isInherit && !_.isEmpty(this.serverless.service.provider.iamRoleStatements)) { // add global statements | ||
for (const s of this.serverless.service.provider.iamRoleStatements) { | ||
@@ -304,4 +369,4 @@ policyStatements.push(s); | ||
} | ||
//add iamRoleStatements | ||
if(_.isArray(functionObject.iamRoleStatements)) { | ||
// add iamRoleStatements | ||
if (_.isArray(functionObject.iamRoleStatements)) { | ||
for (const s of functionObject.iamRoleStatements) { | ||
@@ -311,4 +376,6 @@ policyStatements.push(s); | ||
} | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName || this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) + globalRoleName; | ||
functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName | ||
|| this.getFunctionRoleName(functionName); | ||
const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) | ||
+ globalRoleName; | ||
this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; | ||
@@ -320,10 +387,12 @@ const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); | ||
/** | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role then adjust the DependsOn | ||
* @param functionToRoleMap | ||
* Go over each EventSourceMapping and if it is for a function with a function level iam role | ||
* then adjust the DependsOn | ||
* @param {Map} functionToRoleMap | ||
* @returns void | ||
*/ | ||
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)) { | ||
if (mapping.Type && mapping.Type === 'AWS::Lambda::EventSourceMapping') { | ||
const functionNameFn = _.get(mapping, 'Properties.FunctionName.Fn::GetAtt'); | ||
if (!_.isArray(functionNameFn)) { | ||
continue; | ||
@@ -333,3 +402,3 @@ } | ||
const roleName = functionToRoleMap.get(functionName); | ||
if(roleName) { | ||
if (roleName) { | ||
mapping.DependsOn = roleName; | ||
@@ -341,5 +410,8 @@ } | ||
/** | ||
* @returns void | ||
*/ | ||
createRolesPerFunction() { | ||
const allFunctions = this.serverless.service.getAllFunctions(); | ||
if(_.isEmpty(allFunctions)) { | ||
if (_.isEmpty(allFunctions)) { | ||
return; | ||
@@ -346,0 +418,0 @@ } |
{ | ||
"service": "test-service", | ||
"service": "test-service", | ||
"provider": { | ||
@@ -18,3 +18,3 @@ "stage": "dev", | ||
] | ||
}, | ||
}, | ||
"functions": { | ||
@@ -50,3 +50,3 @@ "hello": { | ||
"events": [], | ||
"name": "test-python-dev-hello", | ||
"name": "test-python-dev-hello-inherit", | ||
"package": {}, | ||
@@ -95,5 +95,5 @@ "vpc": {} | ||
"helloNoPerFunction": { | ||
"handler": "handler.hello", | ||
"handler": "handler.hello", | ||
"events": [], | ||
"name": "test-python-dev-hello", | ||
"name": "test-python-dev-hello-no-per-function", | ||
"package": {}, | ||
@@ -104,5 +104,5 @@ "vpc": {} | ||
"handler": "handler.hello", | ||
"iamRoleStatements": [], | ||
"iamRoleStatements": [], | ||
"events": [], | ||
"name": "test-python-dev-hello", | ||
"name": "test-python-dev-hello-empty-iam-statements", | ||
"package": {}, | ||
@@ -112,3 +112,3 @@ "vpc": { | ||
"subnetIds": ["subnet-xxxx", "subnet-yyyy"] | ||
} | ||
} | ||
} | ||
@@ -115,0 +115,0 @@ }, |
@@ -1,6 +0,3 @@ | ||
// tslint:disable:no-var-requires | ||
import {assert} from 'chai'; | ||
import Plugin from '../lib/index'; | ||
const Serverless = require('serverless/lib/Serverless'); | ||
const funcWithIamTemplate = require('../../src/test/funcs-with-iam.json'); | ||
import _ from 'lodash'; | ||
@@ -11,2 +8,5 @@ import os from 'os'; | ||
const Serverless = require('serverless/lib/Serverless'); | ||
const funcWithIamTemplate = require('../../src/test/funcs-with-iam.json'); | ||
describe('plugin tests', function(this: any) { | ||
@@ -23,5 +23,5 @@ | ||
try { | ||
fs.mkdirSync(dir); | ||
fs.mkdirSync(dir); | ||
} catch (error) { | ||
if(error.code !== 'EEXIST') { | ||
if (error.code !== 'EEXIST') { | ||
console.log('failed to create dir: %s, error: ', dir, error); | ||
@@ -32,4 +32,4 @@ throw error; | ||
const packageFile = path.join(dir, funcWithIamTemplate.package.artifact); | ||
fs.writeFileSync(packageFile, "test123"); | ||
console.log('### serverless version: %s ###', (new Serverless()).version); | ||
fs.writeFileSync(packageFile, 'test123'); | ||
console.log('### serverless version: %s ###', (new Serverless()).version); | ||
}); | ||
@@ -48,16 +48,21 @@ | ||
serverless.pluginManager.loadAllPlugins(); | ||
let compile_hooks: any[] = serverless.pluginManager.getHooks('package:setupProviderConfiguration'); | ||
compile_hooks = compile_hooks.concat( | ||
serverless.pluginManager.getHooks('package:compileFunctions'), | ||
serverless.pluginManager.getHooks('package:compileEvents')); | ||
for (const ent of compile_hooks) { | ||
let compileHooks: any[] = serverless.pluginManager.getHooks('package:setupProviderConfiguration'); | ||
compileHooks = compileHooks.concat( | ||
serverless.pluginManager.getHooks('package:compileFunctions'), | ||
serverless.pluginManager.getHooks('package:compileEvents')); | ||
for (const ent of compileHooks) { | ||
try { | ||
await ent.hook(); | ||
await ent.hook(); | ||
} catch (error) { | ||
console.log("failed running compileFunction hook: [%s] with error: ", ent, error); | ||
console.log('failed running compileFunction hook: [%s] with error: ', ent, error); | ||
assert.fail(); | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
/** | ||
* @param {string} name | ||
* @param {*} roleNameObj | ||
* @returns void | ||
*/ | ||
function assertFunctionRoleName(name: string, roleNameObj: any) { | ||
@@ -67,7 +72,7 @@ assert.isArray(roleNameObj['Fn::Join']); | ||
} | ||
describe('defaultInherit not set', () => { | ||
describe('defaultInherit not set', () => { | ||
let plugin: Plugin; | ||
beforeEach(async () => { | ||
beforeEach(async () => { | ||
plugin = new Plugin(serverless); | ||
@@ -79,5 +84,9 @@ }); | ||
assert.instanceOf(plugin, Plugin); | ||
}); | ||
}); | ||
it('defaultInherit shuuld be false', () => { | ||
it('should NOT initialize the plugin for non AWS providers', () => { | ||
assert.throws(() => new Plugin({ service: { provider: { name: 'not-aws' } } })); | ||
}); | ||
it('defaultInherit should be false', () => { | ||
assert.isFalse(plugin.defaultInherit); | ||
@@ -88,12 +97,12 @@ }); | ||
const statements = [{ | ||
Effect: "Allow", | ||
Effect: 'Allow', | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
], | ||
Resource: '*', | ||
}]; | ||
describe('#validateStatements', () => { | ||
it('should validate valid statement', () => { | ||
it('should validate valid statement', () => { | ||
assert.doesNotThrow(() => {plugin.validateStatements(statements);}); | ||
@@ -103,60 +112,71 @@ }); | ||
it('should throw an error for invalid statement', () => { | ||
const bad_statement = [{ //missing effect | ||
const badStatement = [{ // missing effect | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: "*", | ||
}]; | ||
assert.throws(() => {plugin.validateStatements(bad_statement);}); | ||
], | ||
Resource: '*', | ||
}]; | ||
assert.throws(() => {plugin.validateStatements(badStatement);}); | ||
}); | ||
it('should throw an error for non array type of statement', () => { | ||
const badStatement = { // missing effect | ||
Action: [ | ||
'xray:PutTelemetryRecords', | ||
'xray:PutTraceSegments', | ||
], | ||
Resource: '*', | ||
}; | ||
assert.throws(() => {plugin.validateStatements(badStatement);}); | ||
}); | ||
}); | ||
describe('#getRoleNameLength', () => { | ||
it('Should calculate the acurate role name length us-east-1', () => { | ||
it('Should calculate the accurate role name length us-east-1', () => { | ||
serverless.service.provider.region = 'us-east-1'; | ||
let function_name = 'a'.repeat(10); | ||
let name_parts = [ | ||
const functionName = 'a'.repeat(10); | ||
const nameParts = [ | ||
serverless.service.service, // test-service , length of 12 | ||
serverless.service.provider.stage, // dev, length of 3 : 15 | ||
{ Ref: 'AWS::Region' }, // us-east-1, length 9 : 24 | ||
function_name, // 'a'.repeat(10), length 10 : 34 | ||
'lambdaRole' // lambdaRole, length 10 : 44 | ||
functionName, // 'a'.repeat(10), length 10 : 34 | ||
'lambdaRole', // lambdaRole, length 10 : 44 | ||
]; | ||
let role_name_length = plugin.getRoleNameLength(name_parts) | ||
let expected = 44 // 12 + 3 + 9 + 10 + 10 == 44 | ||
assert.equal(role_name_length, expected + name_parts.length - 1); | ||
const roleNameLength = plugin.getRoleNameLength(nameParts); | ||
const expected = 44; // 12 + 3 + 9 + 10 + 10 == 44 | ||
assert.equal(roleNameLength, expected + nameParts.length - 1); | ||
}); | ||
it('Should calculate the acurate role name length ap-northeast-1', () => { | ||
it('Should calculate the accurate role name length ap-northeast-1', () => { | ||
serverless.service.provider.region = 'ap-northeast-1'; | ||
let function_name = 'a'.repeat(10); | ||
let name_parts = [ | ||
const functionName = 'a'.repeat(10); | ||
const nameParts = [ | ||
serverless.service.service, // test-service , length of 12 | ||
serverless.service.provider.stage, // dev, length of 3 | ||
{ Ref: 'AWS::Region' }, // ap-northeast-1, length 14 | ||
function_name, // 'a'.repeat(10), length 10 | ||
'lambdaRole' // lambdaRole, length 10 | ||
functionName, // 'a'.repeat(10), length 10 | ||
'lambdaRole', // lambdaRole, length 10 | ||
]; | ||
let role_name_length = plugin.getRoleNameLength(name_parts) | ||
let expected = 49 // 12 + 3 + 14 + 10 + 10 == 49 | ||
assert.equal(role_name_length, expected + name_parts.length - 1); | ||
const roleNameLength = plugin.getRoleNameLength(nameParts); | ||
const expected = 49; // 12 + 3 + 14 + 10 + 10 == 49 | ||
assert.equal(roleNameLength, expected + nameParts.length - 1); | ||
}); | ||
it('Should calculate the actual length for a non AWS::Region ref to maintain backward compatability', () => { | ||
it('Should calculate the actual length for a non AWS::Region ref to maintain backward compatibility', () => { | ||
serverless.service.provider.region = 'ap-northeast-1'; | ||
let function_name = 'a'.repeat(10); | ||
let name_parts = [ | ||
const functionName = 'a'.repeat(10); | ||
const nameParts = [ | ||
serverless.service.service, // test-service , length of 12 | ||
{ Ref: 'bananas'}, // bananas, length of 7 | ||
{ Ref: 'AWS::Region' }, // ap-northeast-1, length 14 | ||
function_name, // 'a'.repeat(10), length 10 | ||
'lambdaRole' // lambdaRole, length 10 | ||
functionName, // 'a'.repeat(10), length 10 | ||
'lambdaRole', // lambdaRole, length 10 | ||
]; | ||
let role_name_length = plugin.getRoleNameLength(name_parts) | ||
let expected = 53 // 12 + 7 + 14 + 10 + 10 == 53 | ||
assert.equal(role_name_length, expected + name_parts.length - 1); | ||
const roleNameLength = plugin.getRoleNameLength(nameParts); | ||
const expected = 53; // 12 + 7 + 14 + 10 + 10 == 53 | ||
assert.equal(roleNameLength, expected + nameParts.length - 1); | ||
}); | ||
}); | ||
describe('#getFunctionRoleName', () => { | ||
@@ -167,17 +187,17 @@ it('should return a name with the function name', () => { | ||
assertFunctionRoleName(name, roleName); | ||
const name_parts = roleName['Fn::Join'][1]; | ||
assert.equal(name_parts[name_parts.length - 1], 'lambdaRole'); | ||
const nameParts = roleName['Fn::Join'][1]; | ||
assert.equal(nameParts[nameParts.length - 1], 'lambdaRole'); | ||
}); | ||
it('should throw an error on long name', () => { | ||
const long_name = 'long-long-long-long-long-long-long-long-long-long-long-long-long-name'; | ||
assert.throws(() => {plugin.getFunctionRoleName(long_name);}); | ||
const longName = 'long-long-long-long-long-long-long-long-long-long-long-long-long-name'; | ||
assert.throws(() => {plugin.getFunctionRoleName(longName);}); | ||
try { | ||
plugin.getFunctionRoleName(long_name); | ||
plugin.getFunctionRoleName(longName); | ||
} catch (error) { | ||
//some validation that the error we throw is what we expect | ||
// some validation that the error we throw is what we expect | ||
const msg: string = error.message; | ||
assert.isString(msg); | ||
assert.isString(msg); | ||
assert.isTrue(msg.startsWith('serverless-iam-roles-per-function: ERROR:')); | ||
assert.isTrue(msg.includes(long_name)); | ||
assert.isTrue(msg.includes(longName)); | ||
assert.isTrue(msg.endsWith('iamRoleStatementsName.')); | ||
@@ -187,12 +207,33 @@ } | ||
it('should throw with invalid Fn:Join statement', () => { | ||
assert.throws(() => { | ||
const longName = 'test-name'; | ||
const invalidRoleName = { | ||
'Fn::Join': [], | ||
}; | ||
const slsMock = { | ||
service: { | ||
provider: { | ||
name: 'aws', | ||
}, | ||
}, | ||
providers: { | ||
aws: { naming: { getRoleName: () => invalidRoleName } }, | ||
}, | ||
}; | ||
(new Plugin(slsMock)).getFunctionRoleName(longName); | ||
}); | ||
}); | ||
it('should return a name without "lambdaRole"', () => { | ||
let name = 'test-name'; | ||
let name = 'test-name'; | ||
let roleName = plugin.getFunctionRoleName(name); | ||
const len = plugin.getRoleNameLength(roleName['Fn::Join'][1]); | ||
//create a name which causes role name to be longer than 64 chars by 1. Will cause then lambdaRole to be removed | ||
// create a name which causes role name to be longer than 64 chars by 1. | ||
// Will cause then lambdaRole to be removed | ||
name += 'a'.repeat(64 - len + 1); | ||
roleName = plugin.getFunctionRoleName(name); | ||
assertFunctionRoleName(name, roleName); | ||
const name_parts = roleName['Fn::Join'][1]; | ||
assert.notEqual(name_parts[name_parts.length - 1], 'lambdaRole'); | ||
const nameParts = roleName['Fn::Join'][1]; | ||
assert.notEqual(nameParts[nameParts.length - 1], 'lambdaRole'); | ||
}); | ||
@@ -204,76 +245,106 @@ }); | ||
plugin.createRolesPerFunction(); | ||
const helloRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloIamRoleLambdaExecution; | ||
const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; | ||
const helloRole = compiledResources.HelloIamRoleLambdaExecution; | ||
assert.isNotEmpty(helloRole); | ||
assertFunctionRoleName('hello', helloRole.Properties.RoleName); | ||
assert.isEmpty(helloRole.Properties.ManagedPolicyArns, 'function resource role has no managed policy'); | ||
//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; | ||
// check depends and role is set properly | ||
const helloFunctionResource = compiledResources.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 = compiledResources.HelloInheritIamRoleLambdaExecution; | ||
assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); | ||
let policy_statements: any[] = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject(policy_statements.find((s) => s.Action[0] === "xray:PutTelemetryRecords"), 'global statements imported upon inherit'); | ||
assert.isObject(policy_statements.find((s) => s.Action[0] === "dynamodb:GetItem"), 'per function statements imported upon inherit'); | ||
const streamHandlerRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.StreamHandlerIamRoleLambdaExecution; | ||
let policyStatements: any[] = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject( | ||
policyStatements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords'), | ||
'global statements imported upon inherit', | ||
); | ||
assert.isObject( | ||
policyStatements.find((s) => s.Action[0] === 'dynamodb:GetItem'), | ||
'per function statements imported upon inherit', | ||
); | ||
const streamHandlerRole = compiledResources.StreamHandlerIamRoleLambdaExecution; | ||
assertFunctionRoleName('streamHandler', streamHandlerRole.Properties.RoleName); | ||
policy_statements = streamHandlerRole.Properties.Policies[0].PolicyDocument.Statement; | ||
policyStatements = streamHandlerRole.Properties.Policies[0].PolicyDocument.Statement; | ||
assert.isObject( | ||
policy_statements.find((s) => | ||
policyStatements.find((s) => | ||
_.isEqual(s.Action, [ | ||
"dynamodb:GetRecords", | ||
"dynamodb:GetShardIterator", | ||
"dynamodb:DescribeStream", | ||
"dynamodb:ListStreams"]) && | ||
'dynamodb:GetRecords', | ||
'dynamodb:GetShardIterator', | ||
'dynamodb:DescribeStream', | ||
'dynamodb:ListStreams']) && | ||
_.isEqual(s.Resource, [ | ||
"arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151"])), | ||
'stream statements included' | ||
'arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151'])), | ||
'stream statements included', | ||
); | ||
assert.isObject(policy_statements.find((s) => s.Action[0] === "sns:Publish"), 'sns dlq statements included'); | ||
const streamMapping = serverless.service.provider.compiledCloudFormationTemplate.Resources.StreamHandlerEventSourceMappingDynamodbTest; | ||
assert.equal(streamMapping.DependsOn, "StreamHandlerIamRoleLambdaExecution"); | ||
//verify sqsHandler should have SQS permissions | ||
const sqsHandlerRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.SqsHandlerIamRoleLambdaExecution; | ||
assert.isObject(policyStatements.find((s) => s.Action[0] === 'sns:Publish'), 'sns dlq statements included'); | ||
const streamMapping = compiledResources.StreamHandlerEventSourceMappingDynamodbTest; | ||
assert.equal(streamMapping.DependsOn, 'StreamHandlerIamRoleLambdaExecution'); | ||
// verify sqsHandler should have SQS permissions | ||
const sqsHandlerRole = compiledResources.SqsHandlerIamRoleLambdaExecution; | ||
assertFunctionRoleName('sqsHandler', sqsHandlerRole.Properties.RoleName); | ||
policy_statements = sqsHandlerRole.Properties.Policies[0].PolicyDocument.Statement; | ||
JSON.stringify(policy_statements); | ||
policyStatements = sqsHandlerRole.Properties.Policies[0].PolicyDocument.Statement; | ||
JSON.stringify(policyStatements); | ||
assert.isObject( | ||
policy_statements.find((s) => | ||
policyStatements.find((s) => | ||
_.isEqual(s.Action, [ | ||
"sqs:ReceiveMessage", | ||
"sqs:DeleteMessage", | ||
"sqs:GetQueueAttributes"]) && | ||
'sqs:ReceiveMessage', | ||
'sqs:DeleteMessage', | ||
'sqs:GetQueueAttributes']) && | ||
_.isEqual(s.Resource, [ | ||
"arn:aws:sqs:us-east-1:1234567890:MyQueue", | ||
"arn:aws:sqs:us-east-1:1234567890:MyOtherQueue"])), | ||
'sqs statements included' | ||
'arn:aws:sqs:us-east-1:1234567890:MyQueue', | ||
'arn:aws:sqs:us-east-1:1234567890:MyOtherQueue'])), | ||
'sqs statements included', | ||
); | ||
assert.isObject(policy_statements.find((s) => s.Action[0] === "sns:Publish"), 'sns dlq statements included'); | ||
const sqsMapping = serverless.service.provider.compiledCloudFormationTemplate.Resources.SqsHandlerEventSourceMappingSQSMyQueue; | ||
assert.equal(sqsMapping.DependsOn, "SqsHandlerIamRoleLambdaExecution"); | ||
//verify helloNoPerFunction should have global role | ||
const helloNoPerFunctionResource = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloNoPerFunctionLambdaFunction; | ||
//no DependsOn is added when using global role: https://github.com/serverless/serverless/blob/9303d8ecd46059121082c3308e5fe5385e0be38e/lib/plugins/aws/package/compile/functions/index.js#L42 | ||
assert.isFalse(helloNoPerFunctionResource.DependsOn.indexOf('IamRoleLambdaExecution') >= 0, 'function resource depends on global role'); | ||
assert.equal(helloNoPerFunctionResource.Properties.Role["Fn::GetAtt"][0], 'IamRoleLambdaExecution', "function resource role is set to global role"); | ||
//verify helloEmptyIamStatements | ||
const helloEmptyIamStatementsRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloEmptyIamStatementsIamRoleLambdaExecution; | ||
assert.isObject(policyStatements.find((s) => s.Action[0] === 'sns:Publish'), 'sns dlq statements included'); | ||
const sqsMapping = compiledResources.SqsHandlerEventSourceMappingSQSMyQueue; | ||
assert.equal(sqsMapping.DependsOn, 'SqsHandlerIamRoleLambdaExecution'); | ||
// verify helloNoPerFunction should have global role | ||
const helloNoPerFunctionResource = compiledResources.HelloNoPerFunctionLambdaFunction; | ||
// role is the default role generated by the framework | ||
assert.isFalse( | ||
helloNoPerFunctionResource.DependsOn.indexOf('IamRoleLambdaExecution') === 0, | ||
'function resource depends on global role', | ||
); | ||
assert.equal( | ||
helloNoPerFunctionResource.Properties.Role['Fn::GetAtt'][0], | ||
'IamRoleLambdaExecution', | ||
'function resource role is set to global role', | ||
); | ||
// verify helloEmptyIamStatements | ||
const helloEmptyIamStatementsRole = compiledResources.HelloEmptyIamStatementsIamRoleLambdaExecution; | ||
assertFunctionRoleName('helloEmptyIamStatements', helloEmptyIamStatementsRole.Properties.RoleName); | ||
// tslint:disable-next-line:max-line-length | ||
// assert.equal(helloEmptyIamStatementsRole.Properties.ManagedPolicyArns[0], 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole'); | ||
const helloEmptyFunctionResource = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloEmptyIamStatementsLambdaFunction; | ||
assert.isTrue(helloEmptyFunctionResource.DependsOn.indexOf('HelloEmptyIamStatementsIamRoleLambdaExecution') >= 0, 'function resource depends on role'); | ||
assert.equal(helloEmptyFunctionResource.Properties.Role["Fn::GetAtt"][0], 'HelloEmptyIamStatementsIamRoleLambdaExecution', | ||
"function resource role is set properly"); | ||
// assert.equal( | ||
// helloEmptyIamStatementsRole.Properties.ManagedPolicyArns[0], | ||
// 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', | ||
// ); | ||
const helloEmptyFunctionResource = compiledResources.HelloEmptyIamStatementsLambdaFunction; | ||
assert.isTrue( | ||
helloEmptyFunctionResource.DependsOn.indexOf('HelloEmptyIamStatementsIamRoleLambdaExecution') >= 0, | ||
'function resource depends on role', | ||
); | ||
assert.equal( | ||
helloEmptyFunctionResource.Properties.Role['Fn::GetAtt'][0], | ||
'HelloEmptyIamStatementsIamRoleLambdaExecution', | ||
'function resource role is set properly', | ||
); | ||
}); | ||
it('should do nothing when no functions defined', () => { | ||
const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; | ||
serverless.service.functions = {}; | ||
serverless.service.resources = {}; | ||
plugin.createRolesPerFunction(); | ||
for (const key in serverless.service.provider.compiledCloudFormationTemplate.Resources) { | ||
if (key !== 'IamRoleLambdaExecution' && serverless.service.provider.compiledCloudFormationTemplate.Resources.hasOwnProperty(key)) { | ||
const resource = serverless.service.provider.compiledCloudFormationTemplate.Resources[key]; | ||
if(resource.Type === "AWS::IAM::Role") { | ||
assert.fail(resource, undefined, "There shouldn't be extra roles beyond IamRoleLambdaExecution"); | ||
for (const key in compiledResources) { | ||
if (key !== 'IamRoleLambdaExecution' && Object.prototype.hasOwnProperty.call(compiledResources, key)) { | ||
const resource = compiledResources[key]; | ||
if (resource.Type === 'AWS::IAM::Role') { | ||
assert.fail(resource, undefined, 'There shouldn\'t be extra roles beyond IamRoleLambdaExecution'); | ||
} | ||
@@ -285,3 +356,3 @@ } | ||
it('should throw when external role is defined', () => { | ||
_.set(serverless.service, "functions.hello.role", "arn:${AWS::Partition}:iam::0123456789:role/Test"); | ||
_.set(serverless.service, 'functions.hello.role', 'arn:${AWS::Partition}:iam::0123456789:role/Test'); | ||
assert.throws(() => { | ||
@@ -295,3 +366,3 @@ plugin.createRolesPerFunction(); | ||
describe('#throwErorr', () => { | ||
it('should throw formated error', () => { | ||
it('should throw formatted error', () => { | ||
try { | ||
@@ -311,15 +382,15 @@ plugin.throwError('msg :%s', 'testing'); | ||
describe('defaultInherit set', () => { | ||
describe('defaultInherit set', () => { | ||
let plugin: Plugin; | ||
beforeEach(() => { | ||
//set defaultInherit | ||
_.set(serverless.service, "custom.serverless-iam-roles-per-function.defaultInherit", true); | ||
//change helloInherit to false for testing | ||
_.set(serverless.service, "functions.helloInherit.iamRoleStatementsInherit", false); | ||
beforeEach(() => { | ||
// set defaultInherit | ||
_.set(serverless.service, 'custom.serverless-iam-roles-per-function.defaultInherit', true); | ||
// change helloInherit to false for testing | ||
_.set(serverless.service, 'functions.helloInherit.iamRoleStatementsInherit', false); | ||
plugin = new Plugin(serverless); | ||
}); | ||
describe('#constructor()', () => { | ||
it('defaultInherit shuuld be true', () => { | ||
describe('#constructor()', () => { | ||
it('defaultInherit should be true', () => { | ||
assert.isTrue(plugin.defaultInherit); | ||
@@ -331,23 +402,37 @@ }); | ||
it('should create role per function', () => { | ||
const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; | ||
plugin.createRolesPerFunction(); | ||
const helloRole = serverless.service.provider.compiledCloudFormationTemplate.Resources.HelloIamRoleLambdaExecution; | ||
const helloRole = compiledResources.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"); | ||
// check depends and role is set properlly | ||
const helloFunctionResource = compiledResources.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; | ||
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 = compiledResources.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, | ||
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
84108
1359
166
20
Updatedlodash@^4.17.20