New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

serverless-iam-roles-per-function

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

serverless-iam-roles-per-function - npm Package Compare versions

Comparing version 3.0.0-d84bffd to 3.0.1-21342a9

16

CHANGELOG.md

@@ -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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc