├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── CODEOWNERS ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── github_deploy_key.enc ├── package-lock.json ├── package.json ├── pre-release-version.sh ├── push-release-tag.sh ├── src ├── declarations.ts ├── lib │ └── index.ts └── test │ ├── funcs-with-iam.json │ └── index.test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_size = 2 16 | 17 | [*.json] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | #dist is only for the package 4 | dist 5 | #ignore npm pack output 6 | serverless-iam-roles-per-function-*.tgz 7 | #ignore coverage dir 8 | coverage 9 | .nyc_output 10 | npm-debug.log 11 | *.txt 12 | # JetBrains IDE 13 | .idea 14 | # serverless 15 | .serverless 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @see https://eslint.org/docs/user-guide/configuring#configuring-rules 2 | 3 | const OFF = 0; 4 | const WARN = 1; 5 | const ERROR = 2; 6 | 7 | module.exports = { 8 | root: true, 9 | parser: '@typescript-eslint/parser', 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | ], 15 | env: { 16 | node: true, 17 | mocha: true, 18 | }, 19 | plugins: [ 20 | 'import', 21 | ], 22 | parserOptions: { 23 | ecmaVersion: 2020, 24 | sourceType: 'module', 25 | }, 26 | rules: { 27 | '@typescript-eslint/no-explicit-any': OFF, 28 | '@typescript-eslint/explicit-module-boundary-types': OFF, 29 | '@typescript-eslint/no-unused-vars': [ERROR, { argsIgnorePattern: '^_' }], 30 | // eslint-disable-next-line max-len 31 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md 32 | 'no-unused-vars': OFF, 33 | 'camelcase': ERROR, 34 | 'space-infix-ops': ERROR, 35 | 'keyword-spacing': ERROR, 36 | 'space-before-blocks': ERROR, 37 | 'spaced-comment': ERROR, 38 | 'arrow-body-style': [ERROR, 'as-needed'], 39 | 'comma-dangle': [ERROR, 'always-multiline'], 40 | 'import/imports-first': ERROR, 41 | 'import/newline-after-import': ERROR, 42 | 'import/no-named-as-default': ERROR, 43 | 'import/no-unresolved': [ERROR, { commonjs: true, amd: true }], 44 | 'import/no-cycle': ['error', { maxDepth: 9999 }], 45 | indent: [ERROR, 2, { SwitchCase: 1 }], 46 | 'max-len': [ERROR, 120], 47 | 'newline-per-chained-call': ERROR, 48 | 'no-confusing-arrow': ERROR, 49 | 'no-use-before-define': ERROR, 50 | 'require-yield': ERROR, 51 | 'function-call-argument-newline': [ERROR, 'consistent'], 52 | 'linebreak-style': OFF, 53 | 'no-trailing-spaces': ERROR, 54 | 'no-cond-assign': [ERROR, 'except-parens'], 55 | 'no-unused-expressions': [ERROR, { allowShortCircuit: true, allowTernary: true }], 56 | 'sort-imports': [ERROR, { 57 | ignoreCase: false, 58 | ignoreDeclarationSort: true, 59 | ignoreMemberSort: false, 60 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 61 | }], 62 | eqeqeq: [ERROR, 'always', { null: 'ignore' }], 63 | quotes: [ERROR, 'single'], 64 | // JSDoc Requirements 65 | 'require-jsdoc': [WARN, { 66 | require: { 67 | FunctionDeclaration: true, 68 | MethodDefinition: true, 69 | ClassDeclaration: false, 70 | }, 71 | }], 72 | 'valid-jsdoc': [ERROR, { 73 | requireReturn: true, 74 | requireReturnDescription: false, 75 | requireParamDescription: false, 76 | requireParamType: true, 77 | requireReturnType: false, 78 | preferType: { 79 | Boolean: 'boolean', Number: 'number', object: 'Object', String: 'string', 80 | }, 81 | }], 82 | }, 83 | settings: { 84 | 'import/resolver': { 85 | 'node': { 86 | 'extensions': ['.js', '.jsx', '.ts', '.tsx'], 87 | }, 88 | }, 89 | }, 90 | overrides: [ 91 | { 92 | files: ['*.test.ts'], 93 | rules: { 94 | '@typescript-eslint/no-var-requires': OFF, 95 | }, 96 | }, 97 | ], 98 | }; 99 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # CODEOWNERS 2 | # How this file is used? See: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | /.travis.yml @glicht 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | #dist is only for the package 4 | dist 5 | #ignore npm pack output 6 | serverless-iam-roles-per-function-*.tgz 7 | #ignore coverage dir 8 | coverage 9 | .nyc_output 10 | npm-debug.log 11 | *.txt 12 | # JetBrains IDE 13 | .idea 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #files are managed in package.json at the files section -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | directories: 5 | - $HOME/.npm 6 | 7 | node_js: 8 | - "10" 9 | - "12" 10 | - "14" 11 | 12 | env: 13 | - SERVERLESS_VERSION=latest COV_PUB=true DEPLOY=true 14 | - SERVERLESS_VERSION=latest~1 15 | - SERVERLESS_VERSION=latest~2 16 | 17 | before_install: 18 | - npm i -g npm@6 19 | 20 | install: 21 | - travis_retry npm install 22 | - travis_retry npm install --no-save --ignore-scripts `npx npm-get-version serverless@$SERVERLESS_VERSION` 23 | 24 | script: 25 | - npm run lint 26 | - npm test 27 | 28 | after_success: test -z "$COV_PUB" || npm run coverage 29 | 30 | before_deploy: 31 | - ./pre-release-version.sh 32 | - "npm run prepublishOnly" 33 | 34 | deploy: 35 | # automatic pre-release when merging to master 36 | - provider: npm 37 | email: 38 | secure: "5AV2rx/waiIz0z+CDGWEgUkhNkjBpL+cp5FlSFtrvAaL/LsmaZAgZNlTbxuoUD5Td7jL2TLKk3X9f3CrVBmxuUSHglGBFOOpxKVaOI6u2r20+QRpMt0ETh6csqj0ALK+ePfuf70ER4jg81MxTPlc59V6hnHAdUUsRiVPuNASQlj8pByiSpMcjqW6KWVPk1GIUrKzlKUJfBnWRduFV3yk1I2Qm215myrlRQTa5naAUn+2v289nRhWP6qnG3hmkAVfRhsq+ucu6gbEznEASlyTlZ08TH/2BIldH8956DfNmRUkELHizNLbjwoftnY+alW2XRZFGy+KYMLG6X4llxCoWksNqO6fF+qxZPVeitm68TWWFGIKOM0bAelLn1Unb9VKvyuNfrEi/XtVkYMy9SiaUh3fT3P4PGl1kUg7/iZpJWCKIMDWcqiY1UWIjAAIqH5MaVi2UlrTS+l/2bzLfFsQU0GQBxPMowEjAouZdr9nx9bpCoepO7gUSbglnYQ1h3Z/WbnaGRCWhgBziTbKspUh8YyhGAKrVoA/k1G5IPZ0aqXElggLH4tp49M+5imGMJnJPkkQojltbXzr6D2EJIw11/1J32ct4YG4hAnroXsi3tiPksLiywBu3yZKJ2SQlJsV+/QCgrP7PRewdJXpI5bsegw/HBDy3uOGzSN1daG05mw=" 39 | api_key: 40 | secure: "C9YWfhRbR94hNRg7b6GkteP9Jo9yIFdHQDwm/uvSPKDIwX4kZimBRFiNXdcw0POtkLje41e2osQeCXc3YLOFKpAetCJs69nvakEQ5Lb1InWWGt4L7h1qnkPQgeJJnvy8GLIkyqQXHG01HbO8isjQoWsmZIFmhESMEyWqKrQ1qxE3swwrU6MLw/+V/mqLAigKze8OzeMe53V6yOUVqPUo8exO9wL9wxC22S0yeCboe/0LUWWpxQNJ1sb4ahsyT3gz3G7Hdzo/r4Zbd+krJnQKDp959U9HV/TMSFsjKoMWg3QwGCDhikeeCrbWpdH/YGmXrOD3wE1IJ+TiLe3TnKVa86rtCdME2a6yKn/1M3nI7Xw0aRaCTn5qLYgRciC5RIHZ933kNbEK0EhHLT9t/CpZlD60r+DWxtbJLFERVQgIObx83IkaXkWnB3+tt5jYH7SeIEVMDPhHtXz5/9kkiSMFgOvdi6CVNB2rDCKqz/L7TgOOrT80AQhCIjzFcHrm0Q0Ijje8T/is0Ck7H67wMlMOWa4L96Dtkddgr+TFw9d6SdnawLAeYrpmWjg1A1qgCHPQGKwfPgO19Gy8n8RvgL/DpimSMFoF4mmx338a58ueUKIzgQ2ZhGbg0VhhF2pOqMpcNKYWjFh0TYd3GYt3N96EUUWuwy6mOi9RpKQ5W21zqjo=" 41 | tag: next 42 | skip_cleanup: true 43 | on: 44 | branch: master 45 | node: 14 46 | repo: functionalone/serverless-iam-roles-per-function 47 | condition: '"$DEPLOY" = true && "$TRAVIS_PULL_REQUEST" = false' 48 | # automatic production deploy when merging to release 49 | - provider: npm 50 | email: 51 | secure: "5AV2rx/waiIz0z+CDGWEgUkhNkjBpL+cp5FlSFtrvAaL/LsmaZAgZNlTbxuoUD5Td7jL2TLKk3X9f3CrVBmxuUSHglGBFOOpxKVaOI6u2r20+QRpMt0ETh6csqj0ALK+ePfuf70ER4jg81MxTPlc59V6hnHAdUUsRiVPuNASQlj8pByiSpMcjqW6KWVPk1GIUrKzlKUJfBnWRduFV3yk1I2Qm215myrlRQTa5naAUn+2v289nRhWP6qnG3hmkAVfRhsq+ucu6gbEznEASlyTlZ08TH/2BIldH8956DfNmRUkELHizNLbjwoftnY+alW2XRZFGy+KYMLG6X4llxCoWksNqO6fF+qxZPVeitm68TWWFGIKOM0bAelLn1Unb9VKvyuNfrEi/XtVkYMy9SiaUh3fT3P4PGl1kUg7/iZpJWCKIMDWcqiY1UWIjAAIqH5MaVi2UlrTS+l/2bzLfFsQU0GQBxPMowEjAouZdr9nx9bpCoepO7gUSbglnYQ1h3Z/WbnaGRCWhgBziTbKspUh8YyhGAKrVoA/k1G5IPZ0aqXElggLH4tp49M+5imGMJnJPkkQojltbXzr6D2EJIw11/1J32ct4YG4hAnroXsi3tiPksLiywBu3yZKJ2SQlJsV+/QCgrP7PRewdJXpI5bsegw/HBDy3uOGzSN1daG05mw=" 52 | api_key: 53 | secure: "C9YWfhRbR94hNRg7b6GkteP9Jo9yIFdHQDwm/uvSPKDIwX4kZimBRFiNXdcw0POtkLje41e2osQeCXc3YLOFKpAetCJs69nvakEQ5Lb1InWWGt4L7h1qnkPQgeJJnvy8GLIkyqQXHG01HbO8isjQoWsmZIFmhESMEyWqKrQ1qxE3swwrU6MLw/+V/mqLAigKze8OzeMe53V6yOUVqPUo8exO9wL9wxC22S0yeCboe/0LUWWpxQNJ1sb4ahsyT3gz3G7Hdzo/r4Zbd+krJnQKDp959U9HV/TMSFsjKoMWg3QwGCDhikeeCrbWpdH/YGmXrOD3wE1IJ+TiLe3TnKVa86rtCdME2a6yKn/1M3nI7Xw0aRaCTn5qLYgRciC5RIHZ933kNbEK0EhHLT9t/CpZlD60r+DWxtbJLFERVQgIObx83IkaXkWnB3+tt5jYH7SeIEVMDPhHtXz5/9kkiSMFgOvdi6CVNB2rDCKqz/L7TgOOrT80AQhCIjzFcHrm0Q0Ijje8T/is0Ck7H67wMlMOWa4L96Dtkddgr+TFw9d6SdnawLAeYrpmWjg1A1qgCHPQGKwfPgO19Gy8n8RvgL/DpimSMFoF4mmx338a58ueUKIzgQ2ZhGbg0VhhF2pOqMpcNKYWjFh0TYd3GYt3N96EUUWuwy6mOi9RpKQ5W21zqjo=" 54 | tag: latest 55 | skip_cleanup: true 56 | on: 57 | branch: release 58 | node: 14 59 | repo: functionalone/serverless-iam-roles-per-function 60 | condition: '"$DEPLOY" = true && "$TRAVIS_PULL_REQUEST" = false' 61 | 62 | after_deploy: 63 | - openssl aes-256-cbc -K $encrypted_8ebb1ef83f64_key -iv $encrypted_8ebb1ef83f64_iv -in github_deploy_key.enc -out github_deploy_key -d 64 | - ./push-release-tag.sh 65 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | // Name of configuration; appears in the launch configuration drop down menu. 10 | "name": "Mocha test", 11 | // Type of configuration. Possible values: "node", "mono". 12 | "type": "node", 13 | "request": "launch", 14 | // Workspace relative or absolute path to the program. 15 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 16 | // Automatically stop program after launch. 17 | "stopOnEntry": false, 18 | // Command line arguments passed to the program. 19 | //can also pass in a specific grep pattern to run a specific test. Such as: "--grep", "#constructor" 20 | "args": ["--no-timeouts", "${workspaceRoot}/dist/**/*.test.js"], 21 | "sourceMaps": true, 22 | "outFiles": [ 23 | "${workspaceRoot}/dist/**/*.js" 24 | ], 25 | // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. 26 | "cwd": "${workspaceRoot}", 27 | // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. 28 | "runtimeExecutable": null, 29 | // Environment variables passed to the program. (can also try setting to "production") 30 | "env": { 31 | "NODE_ENV": "testing", 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": false, 3 | "typescript.tsdk": "node_modules\\typescript\\lib", 4 | "cSpell.words": [ 5 | "tempdir" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile", 9 | "problemMatcher": [ 10 | "$tsc" 11 | ] 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [3.2.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.1.1...v3.2.0) (2021-03-19) 6 | 7 | 8 | ### Features 9 | 10 | * Support new provider.iam property ([0d3dd37](https://github.com/functionalone/serverless-iam-roles-per-function/commit/0d3dd37328b283cafc92f42dbc16ed37a6ecd7b2)), closes [#73](https://github.com/functionalone/serverless-iam-roles-per-function/issues/73) 11 | 12 | ### [3.1.1](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.1.0...v3.1.1) (2021-01-03) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * change PermissionsBoundary feature to add suport for cloudformation functions ([PR#70](https://github.com/functionalone/serverless-iam-roles-per-function/pull/70)) 18 | 19 | ## [3.1.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.0.2...v3.1.0) (2020-12-17) 20 | 21 | 22 | ### Features 23 | 24 | * Permission boundary ([PR#68](https://github.com/functionalone/serverless-iam-roles-per-function/pull/68)) 25 | 26 | ## [3.0.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.0.1...v3.0.2) (2020-12-04) 27 | 28 | ### Bug Fixes 29 | Add `logs:CreateLogGroup` action to default policy ([#42](https://github.com/functionalone/serverless-iam-roles-per-function/issues/42)) ([b5e1837](https://github.com/functionalone/serverless-iam-roles-per-function/commit/b5e1837)) 30 | ## [3.0.1](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v3.0.0...v3.0.1) (2020-11-28) 31 | 32 | 33 | ### Features 34 | 35 | * Docs: added contributing section ([d9715ba](https://github.com/functionalone/serverless-iam-roles-per-function/commit/d9715ba)) 36 | 37 | ## [3.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.2...v3.0.0) (2020-11-02) 38 | 39 | ### Bug Fixes 40 | * 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)) 41 | 42 | ### Features 43 | * 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)) 44 | * 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)) 45 | * 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) 46 | 47 | ## [2.0.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.1...v2.0.2) (2019-08-30) 48 | 49 | 50 | ### Features 51 | 52 | * update dependencies ([61c04e7](https://github.com/functionalone/serverless-iam-roles-per-function/commit/61c04e7)) 53 | 54 | 55 | ## [2.0.1](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v2.0.0...v2.0.1) (2019-05-10) 56 | 57 | 58 | ### Bug Fixes 59 | * Fix regression when using a vpc with a function ([#24](https://github.com/functionalone/serverless-iam-roles-per-function/issues/24)) 60 | 61 | 62 | 63 | ## [2.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.4...v2.0.0) (2019-04-30) 64 | 65 | 66 | ### Features 67 | * Prevent hard coded aws partition in arn of resources ([#18](https://github.com/functionalone/serverless-iam-roles-per-function/issues/18)) 68 | * Support for SQS event source ([#17](https://github.com/functionalone/serverless-iam-roles-per-function/issues/17)) 69 | 70 | 71 | ## [1.0.4](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.3...v1.0.4) (2018-09-02) 72 | 73 | 74 | ### Features 75 | 76 | * improved formatting for error messages from the plugin ([cea0541](https://github.com/functionalone/serverless-iam-roles-per-function/commit/cea0541)) 77 | * update dependencies ([43641b3](https://github.com/functionalone/serverless-iam-roles-per-function/commit/43641b3)) 78 | * update README with download stats ([f9b0b4a](https://github.com/functionalone/serverless-iam-roles-per-function/commit/f9b0b4a)) 79 | 80 | 81 | 82 | 83 | ## [1.0.3](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.2...v1.0.3) (2018-08-26) 84 | 85 | 86 | ### Features 87 | 88 | * support for auto shortening the role name when default naming scheme exceeds 64 chars ([97284e4](https://github.com/functionalone/serverless-iam-roles-per-function/commit/97284e4)) 89 | * update dependencies ([b16de8d](https://github.com/functionalone/serverless-iam-roles-per-function/commit/b16de8d)) 90 | 91 | 92 | 93 | 94 | ## [1.0.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.1...v1.0.2) (2018-07-28) 95 | 96 | 97 | ### Features 98 | 99 | * update dependencies ([1f5a6ef](https://github.com/functionalone/serverless-iam-roles-per-function/commit/1f5a6ef)) 100 | 101 | 102 | 103 | 104 | ## [1.0.1](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v1.0.0...v1.0.1) (2018-05-30) 105 | 106 | 107 | ### Features 108 | 109 | * fix README with coveralls coverage status ([aa3efe3](https://github.com/functionalone/serverless-iam-roles-per-function/commit/aa3efe3)) 110 | 111 | 112 | 113 | 114 | ## [1.0.0](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.9...v1.0.0) (2018-05-29) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * remove managed policies from cloned role ([942816f](https://github.com/functionalone/serverless-iam-roles-per-function/commit/942816f)) 120 | 121 | 122 | ### Features 123 | 124 | * support for running tests with multiple serverless versions ([0153d79](https://github.com/functionalone/serverless-iam-roles-per-function/commit/0153d79)) 125 | * tests to check empty iam statements array and no-block ([8d601b4](https://github.com/functionalone/serverless-iam-roles-per-function/commit/8d601b4)) 126 | * update dependencies to latest versions ([b4487c3](https://github.com/functionalone/serverless-iam-roles-per-function/commit/b4487c3)) 127 | * update README with code coverage status ([8387371](https://github.com/functionalone/serverless-iam-roles-per-function/commit/8387371)) 128 | 129 | 130 | 131 | 132 | ## [0.1.9](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.8...v0.1.9) (2018-05-26) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * support per function role with an empty iamRoleStatements clause (issue [#9](https://github.com/functionalone/serverless-iam-roles-per-function/issues/9)) ([5a3aadf](https://github.com/functionalone/serverless-iam-roles-per-function/commit/5a3aadf)) 138 | 139 | 140 | ### Features 141 | 142 | * code coverage reporting ([51367c8](https://github.com/functionalone/serverless-iam-roles-per-function/commit/51367c8)) 143 | 144 | 145 | 146 | 147 | ## [0.1.8](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.7...v0.1.8) (2018-05-17) 148 | 149 | 150 | ### Features 151 | 152 | * add travis ci build status ([24399ae](https://github.com/functionalone/serverless-iam-roles-per-function/commit/24399ae)) 153 | 154 | 155 | 156 | 157 | ## [0.1.7](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.6...v0.1.7) (2018-05-16) 158 | 159 | 160 | ### Features 161 | 162 | * add dependencies status to readme ([9eb79e0](https://github.com/functionalone/serverless-iam-roles-per-function/commit/9eb79e0)) 163 | 164 | 165 | 166 | 167 | ## [0.1.6](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.5...v0.1.6) (2018-05-15) 168 | 169 | 170 | ### Features 171 | 172 | * dependencies update ([d36f969](https://github.com/functionalone/serverless-iam-roles-per-function/commit/d36f969)) 173 | 174 | 175 | 176 | 177 | ## [0.1.5](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.4...v0.1.5) (2018-02-25) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * support for auto adding permissions when sns dlq is used (issue [#5](https://github.com/functionalone/serverless-iam-roles-per-function/issues/5)) ([c4c89d6](https://github.com/functionalone/serverless-iam-roles-per-function/commit/c4c89d6)) 183 | 184 | 185 | 186 | 187 | ## [0.1.4](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.3...v0.1.4) (2018-02-23) 188 | 189 | 190 | ### Bug Fixes 191 | 192 | * support for stream based event sources (issue [#3](https://github.com/functionalone/serverless-iam-roles-per-function/issues/3)) ([3b63d49](https://github.com/functionalone/serverless-iam-roles-per-function/commit/3b63d49)) 193 | 194 | 195 | 196 | 197 | ## [0.1.3](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.2...v0.1.3) (2018-02-20) 198 | 199 | 200 | ### Features 201 | 202 | * new configuration to control default inherit or override behaviour ([542175f](https://github.com/functionalone/serverless-iam-roles-per-function/commit/542175f)) 203 | * support custom role names via the property: iamRoleStatementsName ([93cd015](https://github.com/functionalone/serverless-iam-roles-per-function/commit/93cd015)), closes [#2](https://github.com/functionalone/serverless-iam-roles-per-function/issues/2) 204 | 205 | 206 | 207 | 208 | ## [0.1.2](https://github.com/functionalone/serverless-iam-roles-per-function/compare/v0.1.1...v0.1.2) (2018-02-07) 209 | 210 | 211 | ### Bug Fixes 212 | 213 | * remove dependency on local install of serverless framework. issue [#1](https://github.com/functionalone/serverless-iam-roles-per-function/issues/1) ([fcf61aa](https://github.com/functionalone/serverless-iam-roles-per-function/commit/fcf61aa)) 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Functional One, Ltd. http://www.functional.one 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless IAM Roles Per Function Plugin 2 | 3 | [![serverless][sls-image]][sls-url] 4 | [![npm package][npm-image]][npm-url] 5 | [![Build Status][travis-image]][travis-url] 6 | [![Coverage Status][coveralls-image]][coveralls-url] 7 | [![Dependencies Status][david-image]][david-url] 8 | [![Downloads][downloads-image]][npm-url] 9 | 10 | A Serverless plugin to easily define IAM roles per function via the use of `iamRoleStatements` at the function definition block. 11 | 12 | ## Installation 13 | ``` 14 | npm install --save-dev serverless-iam-roles-per-function 15 | ``` 16 | 17 | Or if you want to try out the `next` upcoming version: 18 | ``` 19 | npm install --save-dev serverless-iam-roles-per-function@next 20 | ``` 21 | 22 | Add the plugin to serverless.yml: 23 | 24 | ```yaml 25 | plugins: 26 | - serverless-iam-roles-per-function 27 | ``` 28 | 29 | **Note**: Node 6.10 or higher runtime required. 30 | 31 | ## Usage 32 | 33 | Define `iamRoleStatements` definitions at the function level: 34 | 35 | ```yaml 36 | functions: 37 | func1: 38 | handler: handler.get 39 | iamRoleStatementsName: my-custom-role-name #optional custom role name setting instead of the default generated one 40 | iamRoleStatements: 41 | - Effect: "Allow" 42 | Action: 43 | - dynamodb:GetItem 44 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/mytable" 45 | ... 46 | func2: 47 | handler: handler.put 48 | iamRoleStatements: 49 | - Effect: "Allow" 50 | Action: 51 | - dynamodb:PutItem 52 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/mytable" 53 | ... 54 | ``` 55 | 56 | The plugin will create a dedicated role for each function that has an `iamRoleStatements` definition. It will include the permissions for create and write to CloudWatch logs, stream events and if VPC is defined: `AWSLambdaVPCAccessExecutionRole` will be included (as is done when using `iamRoleStatements` at the provider level). 57 | 58 | if `iamRoleStatements` are not defined at the function level default behavior is maintained and the function will receive the global IAM role. It is possible to define an empty `iamRoleStatements` for a function and then the function will receive a dedicated role with only the permissions needed for CloudWatch and (if needed) stream events and VPC. Example of defining a function with empty `iamRoleStatements` and configured VPC. The function will receive a custom role with CloudWatch logs permissions and the policy `AWSLambdaVPCAccessExecutionRole`: 59 | 60 | ```yaml 61 | functions: 62 | func1: 63 | handler: handler.get 64 | iamRoleStatements: [] 65 | vpc: 66 | securityGroupIds: 67 | - sg-xxxxxx 68 | subnetIds: 69 | - subnet-xxxx 70 | - subnet-xxxxx 71 | ``` 72 | 73 | By default, function level `iamRoleStatements` override the provider level definition. It is also possible to inherit the provider level definition by specifying the option `iamRoleStatementsInherit: true`: 74 | 75 | **serverless >= v2.24.0** 76 | ```yaml 77 | provider: 78 | name: aws 79 | iam: 80 | role: 81 | statements: 82 | - Effect: "Allow" 83 | Action: 84 | - xray:PutTelemetryRecords 85 | - xray:PutTraceSegments 86 | Resource: "*" 87 | ... 88 | functions: 89 | func1: 90 | handler: handler.get 91 | iamRoleStatementsInherit: true 92 | iamRoleStatements: 93 | - Effect: "Allow" 94 | Action: 95 | - dynamodb:GetItem 96 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/mytable" 97 | ``` 98 | 99 | **serverless < v2.24.0** 100 | ```yaml 101 | provider: 102 | name: aws 103 | iamRoleStatements: 104 | - Effect: "Allow" 105 | Action: 106 | - xray:PutTelemetryRecords 107 | - xray:PutTraceSegments 108 | Resource: "*" 109 | ... 110 | functions: 111 | func1: 112 | handler: handler.get 113 | iamRoleStatementsInherit: true 114 | iamRoleStatements: 115 | - Effect: "Allow" 116 | Action: 117 | - dynamodb:GetItem 118 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/mytable" 119 | ``` 120 | 121 | The generated role for `func1` will contain both the statements defined at the provider level and the ones defined at the function level. 122 | 123 | If you wish to change the default behavior to `inherit` instead of `override` it is possible to specify the following custom configuration: 124 | 125 | ```yaml 126 | custom: 127 | serverless-iam-roles-per-function: 128 | defaultInherit: true 129 | ``` 130 | ## Role Names 131 | The plugin uses a naming convention for function roles which is similar to the naming convention used by the Serverless Framework. Function roles are named with the following convention: 132 | ``` 133 | ----lambdaRole 134 | ``` 135 | AWS has a 64 character limit on role names. If the default naming exceeds 64 chars the plugin will remove the suffix: `-lambdaRole` to shorten the name. If it still exceeds 64 chars an error will be thrown containing a message of the form: 136 | ``` 137 | auto generated role name for function: ${functionName} is too long (over 64 chars). 138 | Try setting a custom role name using the property: iamRoleStatementsName. 139 | ``` 140 | In this case you should set the role name using the property `iamRoleStatementsName`. For example: 141 | ```yaml 142 | functions: 143 | func1: 144 | handler: handler.get 145 | iamRoleStatementsName: my-custom-role-name 146 | iamRoleStatements: 147 | - Effect: "Allow" 148 | Action: 149 | - dynamodb:GetItem 150 | Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/mytable" 151 | ... 152 | ``` 153 | 154 | ## PermissionsBoundary 155 | 156 | Define iamPermissionsBoundary definitions at the function level: 157 | 158 | ```yaml 159 | functions: 160 | func1: 161 | handler: handler.get 162 | iamPermissionsBoundary: !Sub arn:aws:iam::xxxxx:policy/your_permissions_boundary_policy 163 | iamRoleStatementsName: my-custom-role-name 164 | iamRoleStatements: 165 | - Effect: "Allow" 166 | Action: 167 | - sqs:* 168 | Resource: "*" 169 | ... 170 | ``` 171 | 172 | You can set permissionsBoundary for all roles with iamGlobalPermissionsBoundary in custom: 173 | 174 | ```yaml 175 | custom: 176 | serverless-iam-roles-per-function: 177 | iamGlobalPermissionsBoundary: !Sub arn:aws:iam::xxxx:policy/permissions-boundary-policy 178 | ``` 179 | 180 | For more information, see [Permissions Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). 181 | 182 | 183 | ## Contributing 184 | Contributions are welcome and appreciated. 185 | 186 | * 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. 187 | * Once ready, open a [PR](https://github.com/functionalone/serverless-iam-roles-per-function/compare) to contribute your code. 188 | * 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/. 189 | * Update the release notes at [CHANGELOG.md](CHANGELOG.md) and bump the version by running: 190 | ``` 191 | npm run release 192 | ``` 193 | * Examine the [CHANGELOG.md](CHANGELOG.md) and update if still required. 194 | * Don't forget to commit the files modified by `npm run release` (we have the auto-commit option disabled by default). 195 | * 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. 196 | * Test your deployed version by installing with the `next` tag. For example: 197 | ``` 198 | npm install --save-dev serverless-iam-roles-per-function@next 199 | ``` 200 | 201 | ## Publishing a Production Release (Maintainers) 202 | Once a contributed PR (or multiple PRs) have been merged into `master`, there is need to publish a production release, after we are sure that the release is stable. Maintainers with commit access to the repository can publish a release by merging into the `release` branch. Steps to follow: 203 | * Verify that the current deployed pre-release version under the `next` tag in npmjs is working properly. Usually, it is best to allow the `next` version to gain traction a week or two before releasing. Also, if the version solves a specific reported issue, ask the community on the issue to test out the `next` version. 204 | * Make sure the version being used in master hasn't been released. This can happen if a PR was merged without bumping the version by running `npm run release`. If the version needs to be advanced, open a PR to advance the version as specified [here](#contributing). 205 | * Open a PR to merge into the `release` branch. Use as a base the `release` branch and compare the `tag` version to `release`. For example: 206 | ![Example PR](https://user-images.githubusercontent.com/1395797/101236848-1866e180-36dd-11eb-9281-6c726d15e4f1.png) 207 | 208 | * Once approved by another maintainer, merge the PR. 209 | * Make sure to check after the Travis CI build completes that the release has been published to the `latest` tag on [nmpjs](https://www.npmjs.com/package/serverless-iam-roles-per-function?activeTab=versions). 210 | 211 | ## More Info 212 | 213 | **Introduction post**: 214 | [Serverless Framework: Defining Per-Function IAM Roles](https://medium.com/@glicht/serverless-framework-defining-per-function-iam-roles-c678fa09f46d) 215 | 216 | 217 | **Note**: Serverless Framework provides support for defining custom IAM roles on a per function level through the use of the `role` property and creating CloudFormation resources, as documented [here](https://serverless.com/framework/docs/providers/aws/guide/iam#custom-iam-roles). This plugin doesn't support defining both the `role` property and `iamRoleStatements` at the function level. 218 | 219 | [npm-image]:https://img.shields.io/npm/v/serverless-iam-roles-per-function.svg 220 | [npm-url]:http://npmjs.org/package/serverless-iam-roles-per-function 221 | [sls-image]:http://public.serverless.com/badges/v3.svg 222 | [sls-url]:http://www.serverless.com 223 | [travis-image]:https://travis-ci.com/functionalone/serverless-iam-roles-per-function.svg?branch=master 224 | [travis-url]:https://travis-ci.com/functionalone/serverless-iam-roles-per-function 225 | [david-image]:https://david-dm.org/functionalone/serverless-iam-roles-per-function/status.svg 226 | [david-url]:https://david-dm.org/functionalone/serverless-iam-roles-per-function 227 | [coveralls-image]:https://coveralls.io/repos/github/functionalone/serverless-iam-roles-per-function/badge.svg?branch=master 228 | [coveralls-url]:https://coveralls.io/github/functionalone/serverless-iam-roles-per-function?branch=master 229 | [downloads-image]:https://img.shields.io/npm/dm/serverless-iam-roles-per-function.svg 230 | 231 | -------------------------------------------------------------------------------- /github_deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/functionalone/serverless-iam-roles-per-function/1bb7ca7da33385ac9b38f70aee9dd9c0da052382/github_deploy_key.enc -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-iam-roles-per-function", 3 | "private": false, 4 | "version": "3.2.0", 5 | "engines": { 6 | "node": ">=10" 7 | }, 8 | "description": "A Serverless plugin to define IAM Role statements as part of the function definition block", 9 | "main": "dist/lib/index.js", 10 | "scripts": { 11 | "clean": "rimraf dist", 12 | "test-bare": "npm run compile && mocha ./dist/test/**/*.test.js", 13 | "test": "nyc mocha --require ts-node/register --require source-map-support/register ./src/test/**/*.test.ts", 14 | "coverage": "nyc report --reporter=text-lcov | coveralls", 15 | "compile": "tsc", 16 | "watch": "tsc -w", 17 | "prepublishOnly": "npm run clean && npm run compile", 18 | "release": "standard-version", 19 | "lint": "eslint ." 20 | }, 21 | "author": "Functional One, Ltd.", 22 | "license": "MIT", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/functionalone/serverless-iam-roles-per-function" 26 | }, 27 | "keywords": [ 28 | "aws", 29 | "lambda", 30 | "aws lambda", 31 | "serverless", 32 | "policy", 33 | "role", 34 | "iam", 35 | "custom", 36 | "permissions", 37 | "security" 38 | ], 39 | "dependencies": { 40 | "lodash": "^4.17.20" 41 | }, 42 | "devDependencies": { 43 | "@serverless/enterprise-plugin": "^4.1.2", 44 | "@types/chai": "^4.2.14", 45 | "@types/lodash": "^4.14.165", 46 | "@types/mocha": "^8.0.4", 47 | "@types/node": "^12.19.6", 48 | "@typescript-eslint/eslint-plugin": "^4.8.2", 49 | "@typescript-eslint/parser": "^4.8.2", 50 | "chai": "^4.2.0", 51 | "coveralls": "^3.1.0", 52 | "eslint": "^7.14.0", 53 | "eslint-plugin-import": "^2.22.1", 54 | "mocha": "^8.2.1", 55 | "npm-get-version": "^1.0.2", 56 | "nyc": "^15.1.0", 57 | "rimraf": "^3.0.2", 58 | "serverless": "^2.12.0", 59 | "source-map-support": "^0.5.19", 60 | "standard-version": "^9.0.0", 61 | "ts-node": "^9.0.0", 62 | "typescript": "^4.1.2" 63 | }, 64 | "files": [ 65 | "dist/index.*", 66 | "dist/lib/**", 67 | "src/", 68 | "*.md" 69 | ], 70 | "nyc": { 71 | "extension": [ 72 | ".ts", 73 | ".tsx" 74 | ], 75 | "include": [ 76 | "src/lib/**" 77 | ], 78 | "exclude": [ 79 | "**/*.d.ts" 80 | ], 81 | "reporter": [ 82 | "html", 83 | "text" 84 | ], 85 | "all": true 86 | }, 87 | "standard-version": { 88 | "skip": { 89 | "tag": true, 90 | "commit": true 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pre-release-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ##### 4 | # Modify package.json and package-lock.json version to include the current commit hash 5 | ##### 6 | 7 | 8 | # exit on errors 9 | set -e 10 | 11 | if [[ "$TRAVIS_BRANCH" == release ]]; then 12 | echo "Not setting pre-release version as this is a build on release" 13 | exit 0 14 | fi 15 | 16 | # IMPORTANT: Make sure when writing sed command to use: sed -i "${INPLACE[@]}" 17 | # to be compatible with mac and linux 18 | # sed on mac requires '' as param and on linux doesn't 19 | if [[ "$(uname)" == Linux ]]; then 20 | INPLACE=() 21 | else 22 | INPLACE=('') 23 | fi 24 | 25 | HASH=$(git rev-parse --short HEAD) 26 | 27 | function replace_version { 28 | node -p "const p = require('./$1'); const fs = require('fs'); p.version = p.version + '-' + '$HASH'; fs.writeFileSync('$1', JSON.stringify(p,null, 2));" > /dev/null 29 | } 30 | 31 | replace_version package.json 32 | replace_version package-lock.json 33 | -------------------------------------------------------------------------------- /push-release-tag.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ##### 4 | # Push a tag of this release if it doesn't exist 5 | # CAlled after deploy. Uses a ssh deploy key: github_deploy_key 6 | # Key was generated: ssh-keygen -t rsa -b 4096 -C "travis-ci" -f github_deploy_key -N '' 7 | # and added as an encrypted file using: travis encrypt-file github_deploy_key github_deploy_key.enc -a before_deploy 8 | ##### 9 | 10 | 11 | # exit on errors 12 | set -e 13 | 14 | git checkout -- package.json 15 | 16 | PKG_VER=v$(node -p "require('./package.json').version") 17 | 18 | echo "Version from package.json: $PKG_VER" 19 | 20 | if npx git-semver-tags | grep "$PKG_VER"; then 21 | echo "$PKG_VER tag already exists. Nothing to do. Skipping." 22 | exit 0 23 | fi 24 | 25 | chmod 600 github_deploy_key 26 | git config --local user.email "builds@travis-ci.com" 27 | git config --local user.name "Travis CI" 28 | git tag "$PKG_VER" 29 | git remote add origin-ssh git@github.com:functionalone/serverless-iam-roles-per-function.git 30 | echo "Git remotes:" 31 | git remote -v 32 | GIT_SSH_COMMAND='ssh -i github_deploy_key -o IdentitiesOnly=yes' git push origin-ssh "$PKG_VER" 33 | -------------------------------------------------------------------------------- /src/declarations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Declarations for modules that don't have types. Need so we can keep working with noImplicitAny=true 3 | */ 4 | declare module 'serverless/lib/plugins/aws/package/lib/mergeIamTemplates'; 5 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import util from 'util'; 3 | 4 | const PLUGIN_NAME = 'serverless-iam-roles-per-function'; 5 | 6 | interface Statement { 7 | Effect: 'Allow' | 'Deny'; 8 | Action: string | string[]; 9 | Resource: string | any[]; 10 | } 11 | 12 | class ServerlessIamPerFunctionPlugin { 13 | 14 | hooks: {[i: string]: () => void}; 15 | serverless: any; 16 | awsPackagePlugin: any; 17 | defaultInherit: boolean; 18 | 19 | readonly PROVIDER_AWS = 'aws'; 20 | 21 | /** 22 | * 23 | * @param {Serverless} serverless - serverless host object 24 | * @param {Object} _options 25 | */ 26 | constructor(serverless: any, _options?: any) { 27 | this.serverless = serverless; 28 | 29 | if (this.serverless.service.provider.name !== this.PROVIDER_AWS) { 30 | throw new this.serverless.classes.Error(`${PLUGIN_NAME} plugin supports only AWS`); 31 | } 32 | 33 | // Added: Schema based validation of service config 34 | // https://github.com/serverless/serverless/releases/tag/v1.78.0 35 | if (this.serverless.configSchemaHandler) { 36 | const newCustomPropSchema = { 37 | type: 'object', 38 | properties: { 39 | [PLUGIN_NAME]: { 40 | type: 'object', 41 | properties: { 42 | defaultInherit: { type: 'boolean' }, 43 | iamGlobalPermissionsBoundary: { $ref: '#/definitions/awsArn' }, 44 | }, 45 | additionalProperties: false, 46 | }, 47 | }, 48 | }; 49 | serverless.configSchemaHandler.defineCustomProperties(newCustomPropSchema); 50 | 51 | // Added: defineFunctionProperties schema extension method 52 | // https://github.com/serverless/serverless/releases/tag/v2.10.0 53 | if (this.serverless.configSchemaHandler.defineFunctionProperties) { 54 | this.serverless.configSchemaHandler.defineFunctionProperties(this.PROVIDER_AWS, { 55 | properties: { 56 | iamRoleStatementsInherit: { type: 'boolean' }, 57 | iamRoleStatementsName: { type: 'string' }, 58 | iamPermissionsBoundary: { $ref: '#/definitions/awsArn' }, 59 | iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' }, 60 | }, 61 | }); 62 | } 63 | } 64 | 65 | this.hooks = { 66 | 'before:package:finalize': this.createRolesPerFunction.bind(this), 67 | }; 68 | this.defaultInherit = _.get(this.serverless.service, `custom.${PLUGIN_NAME}.defaultInherit`, false); 69 | } 70 | 71 | /** 72 | * Utility function which throws an error. The msg will be formatted with args using util.format. 73 | * Error message will be prefixed with ${PLUGIN_NAME}: ERROR: 74 | * @param {string} msg 75 | * @param {*[]} args 76 | * @returns void 77 | */ 78 | throwError(msg: string, ...args: any[]) { 79 | if (!_.isEmpty(args)) { 80 | msg = util.format(msg, args); 81 | } 82 | const errMsg = `${PLUGIN_NAME}: ERROR: ${msg}`; 83 | throw new this.serverless.classes.Error(errMsg); 84 | } 85 | 86 | /** 87 | * @param {*} statements 88 | * @returns void 89 | */ 90 | validateStatements(statements: any): void { 91 | // Verify that iamRoleStatements (if present) is an array of { Effect: ..., 92 | // Action: ..., Resource: ... } objects. 93 | if (_.isEmpty(statements)) { 94 | return; 95 | } 96 | let violationsFound; 97 | if (!Array.isArray(statements)) { 98 | violationsFound = 'it is not an array'; 99 | } else { 100 | const descriptions = statements.map((statement, i) => { 101 | const missing = [ 102 | ['Effect'], 103 | ['Action', 'NotAction'], 104 | ['Resource', 'NotResource'], 105 | ].filter((props) => props.every((prop) => !statement[prop])); 106 | return missing.length === 0 107 | ? null 108 | : `statement ${i} is missing the following properties: ${missing.map((m) => m.join(' / ')).join(', ')}`; 109 | }); 110 | const flawed = descriptions.filter((curr) => curr); 111 | if (flawed.length) { 112 | violationsFound = flawed.join('; '); 113 | } 114 | } 115 | 116 | if (violationsFound) { 117 | const errorMessage = [ 118 | 'iamRoleStatements should be an array of objects,', 119 | ' where each object has Effect, Action / NotAction, Resource / NotResource fields.', 120 | ` Specifically, ${violationsFound}`, 121 | ].join(''); 122 | this.throwError(errorMessage); 123 | } 124 | } 125 | 126 | /** 127 | * @param {*[]} nameParts 128 | * @returns void 129 | */ 130 | getRoleNameLength(nameParts: any[]) { 131 | let length = 0; // calculate the expected length. Sum the length of each part 132 | for (const part of nameParts) { 133 | if (part.Ref) { 134 | if (part.Ref === 'AWS::Region') { 135 | length += this.serverless.service.provider.region.length; 136 | } else { 137 | length += part.Ref.length; 138 | } 139 | } else { 140 | length += part.length; 141 | } 142 | } 143 | length += (nameParts.length - 1); // take into account the dashes between parts 144 | return length; 145 | } 146 | 147 | /** 148 | * @param {string} functionName 149 | * @returns {string} 150 | */ 151 | getFunctionRoleName(functionName: string) { 152 | const roleName = this.serverless.providers.aws.naming.getRoleName(); 153 | const fnJoin = roleName['Fn::Join']; 154 | if (!_.isArray(fnJoin) || fnJoin.length !== 2 || !_.isArray(fnJoin[1]) || fnJoin[1].length < 2) { 155 | this.throwError('Global Role Name is not in expected format. Got name: ' + JSON.stringify(roleName)); 156 | } 157 | fnJoin[1].splice(2, 0, functionName); // insert the function name 158 | if (this.getRoleNameLength(fnJoin[1]) > 64 && fnJoin[1][fnJoin[1].length - 1] === 'lambdaRole') { 159 | // Remove lambdaRole from name to give more space for function name. 160 | fnJoin[1].pop(); 161 | } 162 | if (this.getRoleNameLength(fnJoin[1]) > 64) { // aws limits to 64 chars the role name 163 | this.throwError(`auto generated role name for function: ${functionName} is too long (over 64 chars). 164 | Try setting a custom role name using the property: iamRoleStatementsName.`); 165 | } 166 | return roleName; 167 | } 168 | 169 | /** 170 | * @param {string} functionName 171 | * @param {string} roleName 172 | * @param {string} globalRoleName 173 | * @return the function resource name 174 | */ 175 | updateFunctionResourceRole(functionName: string, roleName: string, globalRoleName: string): string { 176 | const functionResourceName = this.serverless.providers.aws.naming.getLambdaLogicalId(functionName); 177 | const functionResource = this.serverless.service.provider 178 | .compiledCloudFormationTemplate.Resources[functionResourceName]; 179 | 180 | if (_.isEmpty(functionResource) 181 | || _.isEmpty(functionResource.Properties) 182 | || _.isEmpty(functionResource.Properties.Role) 183 | || !_.isArray(functionResource.Properties.Role['Fn::GetAtt']) 184 | || !_.isArray(functionResource.DependsOn) 185 | ) { 186 | this.throwError('Function Resource is not in expected format. For function name: ' + functionName); 187 | } 188 | functionResource.DependsOn = [roleName].concat( 189 | functionResource.DependsOn.filter(((val: any) => val !== globalRoleName )), 190 | ); 191 | functionResource.Properties.Role['Fn::GetAtt'][0] = roleName; 192 | return functionResourceName; 193 | } 194 | 195 | /** 196 | * Get the necessary statement permissions if there are SQS event sources. 197 | * @param {*} functionObject 198 | * @return statement (possibly null) 199 | */ 200 | getSqsStatement(functionObject: any) { 201 | const sqsStatement: Statement = { 202 | Effect: 'Allow', 203 | Action: [ 204 | 'sqs:ReceiveMessage', 205 | 'sqs:DeleteMessage', 206 | 'sqs:GetQueueAttributes', 207 | ], 208 | Resource: [], 209 | }; 210 | for (const event of functionObject.events) { 211 | if (event.sqs) { 212 | const sqsArn = event.sqs.arn || event.sqs; 213 | (sqsStatement.Resource as any[]).push(sqsArn); 214 | } 215 | } 216 | return sqsStatement.Resource.length ? sqsStatement : null; 217 | } 218 | 219 | /** 220 | * Get the necessary statement permissions if there are stream event sources of dynamo or kinesis. 221 | * @param {*} functionObject 222 | * @return array of statements (possibly empty) 223 | */ 224 | getStreamStatements(functionObject: any) { 225 | const res: any[] = []; 226 | if (_.isEmpty(functionObject.events)) { // no events 227 | return res; 228 | } 229 | const dynamodbStreamStatement: Statement = { 230 | Effect: 'Allow', 231 | Action: [ 232 | 'dynamodb:GetRecords', 233 | 'dynamodb:GetShardIterator', 234 | 'dynamodb:DescribeStream', 235 | 'dynamodb:ListStreams', 236 | ], 237 | Resource: [], 238 | }; 239 | const kinesisStreamStatement: Statement = { 240 | Effect: 'Allow', 241 | Action: [ 242 | 'kinesis:GetRecords', 243 | 'kinesis:GetShardIterator', 244 | 'kinesis:DescribeStream', 245 | 'kinesis:ListStreams', 246 | ], 247 | Resource: [], 248 | }; 249 | for (const event of functionObject.events) { 250 | if (event.stream) { 251 | const streamArn = event.stream.arn || event.stream; 252 | const streamType = event.stream.type || streamArn.split(':')[2]; 253 | switch (streamType) { 254 | case 'dynamodb': 255 | (dynamodbStreamStatement.Resource as any[]).push(streamArn); 256 | break; 257 | case 'kinesis': 258 | (kinesisStreamStatement.Resource as any[]).push(streamArn); 259 | break; 260 | default: 261 | this.throwError(`Unsupported stream type: ${streamType} for function: `, functionObject); 262 | } 263 | } 264 | } 265 | if (dynamodbStreamStatement.Resource.length) { 266 | res.push(dynamodbStreamStatement); 267 | } 268 | if (kinesisStreamStatement.Resource.length) { 269 | res.push(kinesisStreamStatement); 270 | } 271 | return res; 272 | } 273 | 274 | /** 275 | * Will check if function has a definition of iamRoleStatements. 276 | * If so will create a new Role for the function based on these statements. 277 | * @param {string} functionName 278 | * @param {Map} functionToRoleMap - populate the map with a mapping from function resource name to role resource name 279 | * @returns void 280 | */ 281 | createRoleForFunction(functionName: string, functionToRoleMap: Map) { 282 | const functionObject = this.serverless.service.getFunction(functionName); 283 | if (functionObject.iamRoleStatements === undefined) { 284 | return; 285 | } 286 | if (functionObject.role) { 287 | this.throwError( 288 | 'Define function with both \'role\' and \'iamRoleStatements\' is not supported. Function name: ' 289 | + functionName, 290 | ); 291 | } 292 | this.validateStatements(functionObject.iamRoleStatements); 293 | // we use the configured role as a template 294 | const globalRoleName = this.serverless.providers.aws.naming.getRoleLogicalId(); 295 | const globalIamRole = this.serverless.service.provider.compiledCloudFormationTemplate.Resources[globalRoleName]; 296 | const functionIamRole = _.cloneDeep(globalIamRole); 297 | // remove the statements 298 | const policyStatements: Statement[] = []; 299 | functionIamRole.Properties.Policies[0].PolicyDocument.Statement = policyStatements; 300 | // set log statements 301 | policyStatements[0] = { 302 | Effect: 'Allow', 303 | Action: ['logs:CreateLogStream', 'logs:CreateLogGroup', 'logs:PutLogEvents'], 304 | Resource: [ 305 | { 306 | 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}' + 307 | `:log-group:${this.serverless.providers.aws.naming.getLogGroupName(functionObject.name)}:*:*`, 308 | }, 309 | ], 310 | }; 311 | // remove managed policies 312 | functionIamRole.Properties.ManagedPolicyArns = []; 313 | // set vpc if needed 314 | if (!_.isEmpty(functionObject.vpc) || !_.isEmpty(this.serverless.service.provider.vpc)) { 315 | functionIamRole.Properties.ManagedPolicyArns = [{ 316 | 'Fn::Join': ['', 317 | [ 318 | 'arn:', 319 | { Ref: 'AWS::Partition' }, 320 | ':iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', 321 | ], 322 | ], 323 | }]; 324 | } 325 | for (const s of this.getStreamStatements(functionObject)) { // set stream statements (if needed) 326 | policyStatements.push(s); 327 | } 328 | const sqsStatement = this.getSqsStatement(functionObject); // set sqs statement (if needed) 329 | if (sqsStatement) { 330 | policyStatements.push(sqsStatement); 331 | } 332 | // set sns publish for DLQ if needed 333 | // currently only sns is supported: https://serverless.com/framework/docs/providers/aws/events/sns#dlq-with-sqs 334 | if (!_.isEmpty(functionObject.onError)) { 335 | policyStatements.push({ 336 | Effect: 'Allow', 337 | Action: [ 338 | 'sns:Publish', 339 | ], 340 | Resource: functionObject.onError, 341 | }); 342 | } 343 | 344 | const isInherit = functionObject.iamRoleStatementsInherit 345 | || (this.defaultInherit && functionObject.iamRoleStatementsInherit !== false); 346 | 347 | // Since serverless 2.24.0 provider.iamRoleStatements is deprecated 348 | // https://github.com/serverless/serverless/blob/master/CHANGELOG.md#2240-2021-02-16 349 | // Support old & new iam statements by checking if `iam` property exists 350 | const providerIamRoleStatements = this.serverless.service.provider.iam 351 | ? this.serverless.service.provider.iam.role?.statements 352 | : this.serverless.service.provider.iamRoleStatements; 353 | 354 | if (isInherit && !_.isEmpty(providerIamRoleStatements)) { // add global statements 355 | for (const s of providerIamRoleStatements) { 356 | policyStatements.push(s); 357 | } 358 | } 359 | // add iamRoleStatements 360 | if (_.isArray(functionObject.iamRoleStatements)) { 361 | for (const s of functionObject.iamRoleStatements) { 362 | policyStatements.push(s); 363 | } 364 | } 365 | 366 | // add iamPermissionsBoundary 367 | const iamPermissionsBoundary = functionObject.iamPermissionsBoundary; 368 | const iamGlobalPermissionsBoundary = 369 | _.get(this.serverless.service, `custom.${PLUGIN_NAME}.iamGlobalPermissionsBoundary`); 370 | 371 | if (iamPermissionsBoundary || iamGlobalPermissionsBoundary) { 372 | functionIamRole.Properties.PermissionsBoundary = iamPermissionsBoundary || iamGlobalPermissionsBoundary; 373 | } 374 | 375 | if (iamGlobalPermissionsBoundary) { 376 | globalIamRole.Properties.PermissionsBoundary = iamGlobalPermissionsBoundary; 377 | } 378 | 379 | functionIamRole.Properties.RoleName = functionObject.iamRoleStatementsName 380 | || this.getFunctionRoleName(functionName); 381 | const roleResourceName = this.serverless.providers.aws.naming.getNormalizedFunctionName(functionName) 382 | + globalRoleName; 383 | this.serverless.service.provider.compiledCloudFormationTemplate.Resources[roleResourceName] = functionIamRole; 384 | const functionResourceName = this.updateFunctionResourceRole(functionName, roleResourceName, globalRoleName); 385 | functionToRoleMap.set(functionResourceName, roleResourceName); 386 | } 387 | 388 | /** 389 | * Go over each EventSourceMapping and if it is for a function with a function level iam role 390 | * then adjust the DependsOn 391 | * @param {Map} functionToRoleMap 392 | * @returns void 393 | */ 394 | setEventSourceMappings(functionToRoleMap: Map) { 395 | for (const mapping of _.values(this.serverless.service.provider.compiledCloudFormationTemplate.Resources)) { 396 | if (mapping.Type && mapping.Type === 'AWS::Lambda::EventSourceMapping') { 397 | const functionNameFn = _.get(mapping, 'Properties.FunctionName.Fn::GetAtt'); 398 | if (!_.isArray(functionNameFn)) { 399 | continue; 400 | } 401 | const functionName = functionNameFn[0]; 402 | const roleName = functionToRoleMap.get(functionName); 403 | if (roleName) { 404 | mapping.DependsOn = roleName; 405 | } 406 | } 407 | } 408 | } 409 | 410 | /** 411 | * @returns void 412 | */ 413 | createRolesPerFunction() { 414 | const allFunctions = this.serverless.service.getAllFunctions(); 415 | if (_.isEmpty(allFunctions)) { 416 | return; 417 | } 418 | const functionToRoleMap: Map = new Map(); 419 | for (const func of allFunctions) { 420 | this.createRoleForFunction(func, functionToRoleMap); 421 | } 422 | this.setEventSourceMappings(functionToRoleMap); 423 | } 424 | } 425 | 426 | export = ServerlessIamPerFunctionPlugin; 427 | -------------------------------------------------------------------------------- /src/test/funcs-with-iam.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": "test-service", 3 | "provider": { 4 | "stage": "dev", 5 | "region": "us-east-1", 6 | "name": "aws", 7 | "runtime": "python2.7", 8 | "iamRoleStatements": [ 9 | { 10 | "Effect": "Allow", 11 | "Action": [ 12 | "xray:PutTelemetryRecords", 13 | "xray:PutTraceSegments" 14 | ], 15 | "Resource": "*" 16 | } 17 | ] 18 | }, 19 | "functions": { 20 | "hello": { 21 | "handler": "handler.hello", 22 | "iamRoleStatements": [ 23 | { 24 | "Effect": "Allow", 25 | "Action": [ 26 | "dynamodb:GetItem" 27 | ], 28 | "Resource": "arn:aws:dynamodb:us-east-1:*:table/test" 29 | } 30 | ], 31 | "events": [], 32 | "name": "test-python-dev-hello", 33 | "package": {}, 34 | "vpc": {} 35 | }, 36 | "helloInherit": { 37 | "handler": "handler.hello", 38 | "iamRoleStatements": [ 39 | { 40 | "Effect": "Allow", 41 | "Action": [ 42 | "dynamodb:GetItem" 43 | ], 44 | "Resource": "arn:aws:dynamodb:us-east-1:*:table/test" 45 | } 46 | ], 47 | "iamRoleStatementsInherit": true, 48 | "events": [], 49 | "name": "test-python-dev-hello-inherit", 50 | "package": {}, 51 | "vpc": {} 52 | }, 53 | "streamHandler": { 54 | "handler": "handler.stream", 55 | "iamRoleStatements": [ 56 | { 57 | "Effect": "Allow", 58 | "Action": [ 59 | "dynamodb:GetItem" 60 | ], 61 | "Resource": "arn:aws:dynamodb:us-east-1:*:table/test" 62 | } 63 | ], 64 | "events": [ 65 | {"stream": "arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151"} 66 | ], 67 | "name": "test-python-dev-stream-handler", 68 | "onError": "arn:aws:sns:us-east-1:1234567890123:lambda-dlq", 69 | "package": {}, 70 | "vpc": {} 71 | }, 72 | "sqsHandler": { 73 | "handler": "handler.sqs", 74 | "iamRoleStatements": [ 75 | { 76 | "Effect": "Allow", 77 | "Action": [ 78 | "dynamodb:GetItem" 79 | ], 80 | "Resource": "arn:aws:dynamodb:us-east-1:*:table/test" 81 | } 82 | ], 83 | "events": [ 84 | {"sqs": "arn:aws:sqs:us-east-1:1234567890:MyQueue"}, 85 | {"sqs": {"arn": "arn:aws:sqs:us-east-1:1234567890:MyOtherQueue"}} 86 | ], 87 | "name": "test-python-dev-sqs-handler", 88 | "onError": "arn:aws:sns:us-east-1:1234567890123:lambda-dlq", 89 | "package": {}, 90 | "vpc": {} 91 | }, 92 | "helloNoPerFunction": { 93 | "handler": "handler.hello", 94 | "events": [], 95 | "name": "test-python-dev-hello-no-per-function", 96 | "package": {}, 97 | "vpc": {} 98 | }, 99 | "helloEmptyIamStatements": { 100 | "handler": "handler.hello", 101 | "iamRoleStatements": [], 102 | "events": [], 103 | "name": "test-python-dev-hello-empty-iam-statements", 104 | "package": {}, 105 | "vpc": { 106 | "securityGroupIds": ["sg-xxxxxx"], 107 | "subnetIds": ["subnet-xxxx", "subnet-yyyy"] 108 | } 109 | }, 110 | "helloPermissionsBoundary": { 111 | "handler": "handler.permissionsBoundary", 112 | "iamRoleStatements": [], 113 | "iamPermissionsBoundary": { 114 | "Fn::Sub": "arn:aws:iam::xxxxx:policy/your_permissions_boundary_policy" 115 | }, 116 | "events": [], 117 | "name": "test-permissions-boundary-hello", 118 | "package": {} 119 | } 120 | }, 121 | "resources": { 122 | "Resources": { 123 | "HelloLambdaFunction": { 124 | "Type": "AWS::Lambda::Function", 125 | "Properties": { 126 | "TracingConfig": { 127 | "Mode": "Active" 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | "package": { 134 | "artifact": "test-service.zip", 135 | "exclude": [ 136 | "node_modules/**", 137 | "package-lock.json" 138 | ], 139 | "artifactDirectoryName": "serverless/test-service/dev/1517233344526-2018-01-29T13:42:24.526Z" 140 | }, 141 | "artifact": "test-service.zip" 142 | } 143 | -------------------------------------------------------------------------------- /src/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import {assert} from 'chai'; 2 | import Plugin from '../lib/index'; 3 | import _ from 'lodash'; 4 | import os from 'os'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | const Serverless = require('serverless/lib/Serverless'); 9 | const funcWithIamTemplate = require('../../src/test/funcs-with-iam.json'); 10 | 11 | describe('plugin tests', function(this: any) { 12 | 13 | this.timeout(15000); 14 | 15 | let serverless: any; 16 | 17 | const tempdir = os.tmpdir(); 18 | 19 | before(() => { 20 | const dir = path.join(tempdir, '.serverless'); 21 | try { 22 | fs.mkdirSync(dir); 23 | } catch (error) { 24 | if (error.code !== 'EEXIST') { 25 | console.log('failed to create dir: %s, error: ', dir, error); 26 | throw error; 27 | } 28 | } 29 | const packageFile = path.join(dir, funcWithIamTemplate.package.artifact); 30 | fs.writeFileSync(packageFile, 'test123'); 31 | console.log('### serverless version: %s ###', (new Serverless()).version); 32 | }); 33 | 34 | beforeEach(async () => { 35 | serverless = new Serverless(); 36 | serverless.cli = new serverless.classes.CLI(); 37 | 38 | // Since serverless 2.24.0 processInput function doesn't exist 39 | if (serverless.cli.processInput) { 40 | serverless.processedInput = serverless.cli.processInput(); 41 | } 42 | 43 | Object.assign(serverless.service, _.cloneDeep(funcWithIamTemplate)); 44 | serverless.service.provider.compiledCloudFormationTemplate = { 45 | Resources: {}, 46 | Outputs: {}, 47 | }; 48 | serverless.config.servicePath = tempdir; 49 | serverless.pluginManager.loadAllPlugins(); 50 | let compileHooks: any[] = serverless.pluginManager.getHooks('package:setupProviderConfiguration'); 51 | compileHooks = compileHooks.concat( 52 | serverless.pluginManager.getHooks('package:compileFunctions'), 53 | serverless.pluginManager.getHooks('package:compileEvents')); 54 | for (const ent of compileHooks) { 55 | try { 56 | await ent.hook(); 57 | } catch (error) { 58 | console.log('failed running compileFunction hook: [%s] with error: ', ent, error); 59 | assert.fail(); 60 | } 61 | } 62 | }); 63 | 64 | /** 65 | * @param {string} name 66 | * @param {*} roleNameObj 67 | * @returns void 68 | */ 69 | function assertFunctionRoleName(name: string, roleNameObj: any) { 70 | assert.isArray(roleNameObj['Fn::Join']); 71 | assert.isTrue(roleNameObj['Fn::Join'][1].toString().indexOf(name) >= 0, 'role name contains function name'); 72 | } 73 | 74 | describe('defaultInherit not set', () => { 75 | let plugin: Plugin; 76 | 77 | beforeEach(async () => { 78 | plugin = new Plugin(serverless); 79 | }); 80 | 81 | describe('#constructor()', () => { 82 | it('should initialize the plugin', () => { 83 | assert.instanceOf(plugin, Plugin); 84 | }); 85 | 86 | it('should NOT initialize the plugin for non AWS providers', () => { 87 | assert.throws(() => new Plugin({ service: { provider: { name: 'not-aws' } } })); 88 | }); 89 | 90 | it('defaultInherit should be false', () => { 91 | assert.isFalse(plugin.defaultInherit); 92 | }); 93 | }); 94 | 95 | const statements = [{ 96 | Effect: 'Allow', 97 | Action: [ 98 | 'xray:PutTelemetryRecords', 99 | 'xray:PutTraceSegments', 100 | ], 101 | Resource: '*', 102 | }]; 103 | 104 | describe('#validateStatements', () => { 105 | it('should validate valid statement', () => { 106 | assert.doesNotThrow(() => {plugin.validateStatements(statements);}); 107 | }); 108 | 109 | it('should throw an error for invalid statement', () => { 110 | const badStatement = [{ // missing effect 111 | Action: [ 112 | 'xray:PutTelemetryRecords', 113 | 'xray:PutTraceSegments', 114 | ], 115 | Resource: '*', 116 | }]; 117 | assert.throws(() => {plugin.validateStatements(badStatement);}); 118 | }); 119 | 120 | it('should throw an error for non array type of statement', () => { 121 | const badStatement = { // missing effect 122 | Action: [ 123 | 'xray:PutTelemetryRecords', 124 | 'xray:PutTraceSegments', 125 | ], 126 | Resource: '*', 127 | }; 128 | assert.throws(() => {plugin.validateStatements(badStatement);}); 129 | }); 130 | }); 131 | 132 | describe('#getRoleNameLength', () => { 133 | it('Should calculate the accurate role name length us-east-1', () => { 134 | serverless.service.provider.region = 'us-east-1'; 135 | const functionName = 'a'.repeat(10); 136 | const nameParts = [ 137 | serverless.service.service, // test-service , length of 12 138 | serverless.service.provider.stage, // dev, length of 3 : 15 139 | { Ref: 'AWS::Region' }, // us-east-1, length 9 : 24 140 | functionName, // 'a'.repeat(10), length 10 : 34 141 | 'lambdaRole', // lambdaRole, length 10 : 44 142 | ]; 143 | const roleNameLength = plugin.getRoleNameLength(nameParts); 144 | const expected = 44; // 12 + 3 + 9 + 10 + 10 == 44 145 | assert.equal(roleNameLength, expected + nameParts.length - 1); 146 | }); 147 | 148 | it('Should calculate the accurate role name length ap-northeast-1', () => { 149 | serverless.service.provider.region = 'ap-northeast-1'; 150 | const functionName = 'a'.repeat(10); 151 | const nameParts = [ 152 | serverless.service.service, // test-service , length of 12 153 | serverless.service.provider.stage, // dev, length of 3 154 | { Ref: 'AWS::Region' }, // ap-northeast-1, length 14 155 | functionName, // 'a'.repeat(10), length 10 156 | 'lambdaRole', // lambdaRole, length 10 157 | ]; 158 | const roleNameLength = plugin.getRoleNameLength(nameParts); 159 | const expected = 49; // 12 + 3 + 14 + 10 + 10 == 49 160 | assert.equal(roleNameLength, expected + nameParts.length - 1); 161 | }); 162 | 163 | it('Should calculate the actual length for a non AWS::Region ref to maintain backward compatibility', () => { 164 | serverless.service.provider.region = 'ap-northeast-1'; 165 | const functionName = 'a'.repeat(10); 166 | const nameParts = [ 167 | serverless.service.service, // test-service , length of 12 168 | { Ref: 'bananas'}, // bananas, length of 7 169 | { Ref: 'AWS::Region' }, // ap-northeast-1, length 14 170 | functionName, // 'a'.repeat(10), length 10 171 | 'lambdaRole', // lambdaRole, length 10 172 | ]; 173 | const roleNameLength = plugin.getRoleNameLength(nameParts); 174 | const expected = 53; // 12 + 7 + 14 + 10 + 10 == 53 175 | assert.equal(roleNameLength, expected + nameParts.length - 1); 176 | }); 177 | }); 178 | 179 | describe('#getFunctionRoleName', () => { 180 | it('should return a name with the function name', () => { 181 | const name = 'test-name'; 182 | const roleName = plugin.getFunctionRoleName(name); 183 | assertFunctionRoleName(name, roleName); 184 | const nameParts = roleName['Fn::Join'][1]; 185 | assert.equal(nameParts[nameParts.length - 1], 'lambdaRole'); 186 | }); 187 | 188 | it('should throw an error on long name', () => { 189 | const longName = 'long-long-long-long-long-long-long-long-long-long-long-long-long-name'; 190 | assert.throws(() => {plugin.getFunctionRoleName(longName);}); 191 | try { 192 | plugin.getFunctionRoleName(longName); 193 | } catch (error) { 194 | // some validation that the error we throw is what we expect 195 | const msg: string = error.message; 196 | assert.isString(msg); 197 | assert.isTrue(msg.startsWith('serverless-iam-roles-per-function: ERROR:')); 198 | assert.isTrue(msg.includes(longName)); 199 | assert.isTrue(msg.endsWith('iamRoleStatementsName.')); 200 | } 201 | }); 202 | 203 | it('should throw with invalid Fn:Join statement', () => { 204 | assert.throws(() => { 205 | const longName = 'test-name'; 206 | const invalidRoleName = { 207 | 'Fn::Join': [], 208 | }; 209 | const slsMock = { 210 | service: { 211 | provider: { 212 | name: 'aws', 213 | }, 214 | }, 215 | providers: { 216 | aws: { naming: { getRoleName: () => invalidRoleName } }, 217 | }, 218 | }; 219 | (new Plugin(slsMock)).getFunctionRoleName(longName); 220 | }); 221 | }); 222 | 223 | it('should return a name without "lambdaRole"', () => { 224 | let name = 'test-name'; 225 | let roleName = plugin.getFunctionRoleName(name); 226 | const len = plugin.getRoleNameLength(roleName['Fn::Join'][1]); 227 | // create a name which causes role name to be longer than 64 chars by 1. 228 | // Will cause then lambdaRole to be removed 229 | name += 'a'.repeat(64 - len + 1); 230 | roleName = plugin.getFunctionRoleName(name); 231 | assertFunctionRoleName(name, roleName); 232 | const nameParts = roleName['Fn::Join'][1]; 233 | assert.notEqual(nameParts[nameParts.length - 1], 'lambdaRole'); 234 | }); 235 | }); 236 | 237 | describe('#createRolesPerFunction', () => { 238 | it('should create role per function', () => { 239 | plugin.createRolesPerFunction(); 240 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 241 | const helloRole = compiledResources.HelloIamRoleLambdaExecution; 242 | assert.isNotEmpty(helloRole); 243 | assertFunctionRoleName('hello', helloRole.Properties.RoleName); 244 | assert.isEmpty(helloRole.Properties.ManagedPolicyArns, 'function resource role has no managed policy'); 245 | // check depends and role is set properly 246 | const helloFunctionResource = compiledResources.HelloLambdaFunction; 247 | assert.isTrue( 248 | helloFunctionResource.DependsOn.indexOf('HelloIamRoleLambdaExecution') >= 0, 249 | 'function resource depends on role', 250 | ); 251 | assert.equal( 252 | helloFunctionResource.Properties.Role['Fn::GetAtt'][0], 253 | 'HelloIamRoleLambdaExecution', 254 | 'function resource role is set properly', 255 | ); 256 | const helloInheritRole = compiledResources.HelloInheritIamRoleLambdaExecution; 257 | assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); 258 | let policyStatements: any[] = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; 259 | assert.isObject( 260 | policyStatements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords'), 261 | 'global statements imported upon inherit', 262 | ); 263 | assert.isObject( 264 | policyStatements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 265 | 'per function statements imported upon inherit', 266 | ); 267 | const streamHandlerRole = compiledResources.StreamHandlerIamRoleLambdaExecution; 268 | assertFunctionRoleName('streamHandler', streamHandlerRole.Properties.RoleName); 269 | policyStatements = streamHandlerRole.Properties.Policies[0].PolicyDocument.Statement; 270 | assert.isObject( 271 | policyStatements.find((s) => 272 | _.isEqual(s.Action, [ 273 | 'dynamodb:GetRecords', 274 | 'dynamodb:GetShardIterator', 275 | 'dynamodb:DescribeStream', 276 | 'dynamodb:ListStreams']) && 277 | _.isEqual(s.Resource, [ 278 | 'arn:aws:dynamodb:us-east-1:1234567890:table/test/stream/2017-10-09T19:39:15.151'])), 279 | 'stream statements included', 280 | ); 281 | assert.isObject(policyStatements.find((s) => s.Action[0] === 'sns:Publish'), 'sns dlq statements included'); 282 | const streamMapping = compiledResources.StreamHandlerEventSourceMappingDynamodbTest; 283 | assert.equal(streamMapping.DependsOn, 'StreamHandlerIamRoleLambdaExecution'); 284 | // verify sqsHandler should have SQS permissions 285 | const sqsHandlerRole = compiledResources.SqsHandlerIamRoleLambdaExecution; 286 | assertFunctionRoleName('sqsHandler', sqsHandlerRole.Properties.RoleName); 287 | policyStatements = sqsHandlerRole.Properties.Policies[0].PolicyDocument.Statement; 288 | JSON.stringify(policyStatements); 289 | assert.isObject( 290 | policyStatements.find((s) => 291 | _.isEqual(s.Action, [ 292 | 'sqs:ReceiveMessage', 293 | 'sqs:DeleteMessage', 294 | 'sqs:GetQueueAttributes']) && 295 | _.isEqual(s.Resource, [ 296 | 'arn:aws:sqs:us-east-1:1234567890:MyQueue', 297 | 'arn:aws:sqs:us-east-1:1234567890:MyOtherQueue'])), 298 | 'sqs statements included', 299 | ); 300 | assert.isObject(policyStatements.find((s) => s.Action[0] === 'sns:Publish'), 'sns dlq statements included'); 301 | const sqsMapping = compiledResources.SqsHandlerEventSourceMappingSQSMyQueue; 302 | assert.equal(sqsMapping.DependsOn, 'SqsHandlerIamRoleLambdaExecution'); 303 | // verify helloNoPerFunction should have global role 304 | const helloNoPerFunctionResource = compiledResources.HelloNoPerFunctionLambdaFunction; 305 | // role is the default role generated by the framework 306 | assert.isFalse( 307 | helloNoPerFunctionResource.DependsOn.indexOf('IamRoleLambdaExecution') === 0, 308 | 'function resource depends on global role', 309 | ); 310 | assert.equal( 311 | helloNoPerFunctionResource.Properties.Role['Fn::GetAtt'][0], 312 | 'IamRoleLambdaExecution', 313 | 'function resource role is set to global role', 314 | ); 315 | // verify helloEmptyIamStatements 316 | const helloEmptyIamStatementsRole = compiledResources.HelloEmptyIamStatementsIamRoleLambdaExecution; 317 | assertFunctionRoleName('helloEmptyIamStatements', helloEmptyIamStatementsRole.Properties.RoleName); 318 | // assert.equal( 319 | // helloEmptyIamStatementsRole.Properties.ManagedPolicyArns[0], 320 | // 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole', 321 | // ); 322 | const helloEmptyFunctionResource = compiledResources.HelloEmptyIamStatementsLambdaFunction; 323 | assert.isTrue( 324 | helloEmptyFunctionResource.DependsOn.indexOf('HelloEmptyIamStatementsIamRoleLambdaExecution') >= 0, 325 | 'function resource depends on role', 326 | ); 327 | assert.equal( 328 | helloEmptyFunctionResource.Properties.Role['Fn::GetAtt'][0], 329 | 'HelloEmptyIamStatementsIamRoleLambdaExecution', 330 | 'function resource role is set properly', 331 | ); 332 | }); 333 | 334 | it('should do nothing when no functions defined', () => { 335 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 336 | serverless.service.functions = {}; 337 | serverless.service.resources = {}; 338 | plugin.createRolesPerFunction(); 339 | for (const key in compiledResources) { 340 | if (key !== 'IamRoleLambdaExecution' && Object.prototype.hasOwnProperty.call(compiledResources, key)) { 341 | const resource = compiledResources[key]; 342 | if (resource.Type === 'AWS::IAM::Role') { 343 | assert.fail(resource, undefined, 'There shouldn\'t be extra roles beyond IamRoleLambdaExecution'); 344 | } 345 | } 346 | } 347 | }); 348 | 349 | it('should throw when external role is defined', () => { 350 | _.set(serverless.service, 'functions.hello.role', 'arn:${AWS::Partition}:iam::0123456789:role/Test'); 351 | assert.throws(() => { 352 | plugin.createRolesPerFunction(); 353 | }); 354 | }); 355 | 356 | }); 357 | 358 | describe('#throwErorr', () => { 359 | it('should throw formatted error', () => { 360 | try { 361 | plugin.throwError('msg :%s', 'testing'); 362 | assert.fail('expected error to be thrown'); 363 | } catch (error) { 364 | const msg: string = error.message; 365 | assert.isString(msg); 366 | assert.isTrue(msg.startsWith('serverless-iam-roles-per-function: ERROR:')); 367 | assert.isTrue(msg.includes('testing')); 368 | } 369 | }); 370 | }); 371 | 372 | }); 373 | 374 | describe('defaultInherit set', () => { 375 | let plugin: Plugin; 376 | 377 | beforeEach(() => { 378 | // set defaultInherit 379 | _.set(serverless.service, 'custom.serverless-iam-roles-per-function.defaultInherit', true); 380 | // change helloInherit to false for testing 381 | _.set(serverless.service, 'functions.helloInherit.iamRoleStatementsInherit', false); 382 | plugin = new Plugin(serverless); 383 | }); 384 | 385 | describe('#constructor()', () => { 386 | it('defaultInherit should be true', () => { 387 | assert.isTrue(plugin.defaultInherit); 388 | }); 389 | }); 390 | 391 | describe('#createRolesPerFunction', () => { 392 | it('should create role per function', () => { 393 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 394 | plugin.createRolesPerFunction(); 395 | const helloRole = compiledResources.HelloIamRoleLambdaExecution; 396 | assert.isNotEmpty(helloRole); 397 | assertFunctionRoleName('hello', helloRole.Properties.RoleName); 398 | // check depends and role is set properlly 399 | const helloFunctionResource = compiledResources.HelloLambdaFunction; 400 | assert.isTrue( 401 | helloFunctionResource.DependsOn.indexOf('HelloIamRoleLambdaExecution') >= 0, 402 | 'function resource depends on role', 403 | ); 404 | assert.equal( 405 | helloFunctionResource.Properties.Role['Fn::GetAtt'][0], 406 | 'HelloIamRoleLambdaExecution', 407 | 'function resource role is set properly', 408 | ); 409 | let statements: any[] = helloRole.Properties.Policies[0].PolicyDocument.Statement; 410 | assert.isObject( 411 | statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords'), 412 | 'global statements imported as defaultInherit is set', 413 | ); 414 | assert.isObject( 415 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 416 | 'per function statements imported upon inherit', 417 | ); 418 | const helloInheritRole = compiledResources.HelloInheritIamRoleLambdaExecution; 419 | assertFunctionRoleName('helloInherit', helloInheritRole.Properties.RoleName); 420 | statements = helloInheritRole.Properties.Policies[0].PolicyDocument.Statement; 421 | assert.isObject(statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 'per function statements imported'); 422 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 423 | 'global statements not imported as iamRoleStatementsInherit is false'); 424 | }); 425 | 426 | it('should add permission policy arn when there is iamPermissionsBoundary defined', () => { 427 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 428 | plugin.createRolesPerFunction(); 429 | const helloPermissionsBoundaryIamRole = compiledResources.HelloPermissionsBoundaryIamRoleLambdaExecution; 430 | const policyName = helloPermissionsBoundaryIamRole.Properties.PermissionsBoundary['Fn::Sub']; 431 | assert.equal(policyName, 'arn:aws:iam::xxxxx:policy/your_permissions_boundary_policy'); 432 | }) 433 | 434 | it('should add permission policy arn when there is iamGlobalPermissionsBoundary defined', () => { 435 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 436 | serverless.service.custom['serverless-iam-roles-per-function'] = { 437 | iamGlobalPermissionsBoundary: { 438 | 'Fn::Sub': 'arn:aws:iam::xxxxx:policy/permissions_boundary', 439 | }, 440 | }; 441 | plugin.createRolesPerFunction(); 442 | const defaultIamRoleLambdaExecution = compiledResources.IamRoleLambdaExecution; 443 | const policyName = defaultIamRoleLambdaExecution.Properties.PermissionsBoundary['Fn::Sub']; 444 | assert.equal(policyName, 'arn:aws:iam::xxxxx:policy/permissions_boundary'); 445 | }) 446 | }); 447 | }); 448 | 449 | describe('support new provider.iam property', () => { 450 | const getLambdaTestStatements = (): any[] => { 451 | const plugin = new Plugin(serverless); 452 | 453 | const compiledResources = serverless.service.provider.compiledCloudFormationTemplate.Resources; 454 | plugin.createRolesPerFunction(); 455 | const helloInherit = compiledResources.HelloInheritIamRoleLambdaExecution; 456 | assert.isNotEmpty(helloInherit); 457 | 458 | return helloInherit.Properties.Policies[0].PolicyDocument.Statement; 459 | } 460 | 461 | it('no global iam and iamRoleStatements properties', () => { 462 | _.set(serverless.service, 'provider.iam', undefined); 463 | _.set(serverless.service, 'provider.iamRoleStatements', undefined); 464 | 465 | const statements = getLambdaTestStatements(); 466 | 467 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 468 | 'provider.iamRoleStatements values shouldn\'t exists'); 469 | assert.isObject( 470 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 471 | 'per function statements imported upon inherit', 472 | ); 473 | }); 474 | 475 | describe('new iam property takes precedence over old iamRoleStatements property', () => { 476 | it('empty iam object', () => { 477 | _.set(serverless.service, 'provider.iam', {}); 478 | 479 | const statements = getLambdaTestStatements(); 480 | 481 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 482 | 'provider.iamRoleStatements values shouldn\'t exists'); 483 | assert.isObject( 484 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 485 | 'per function statements imported upon inherit', 486 | ); 487 | }); 488 | 489 | it('no role property', () => { 490 | _.set(serverless.service, 'provider.iam', { 491 | deploymentRole: 'arn:aws:iam::123456789012:role/deploy-role', 492 | }); 493 | 494 | const statements = getLambdaTestStatements(); 495 | 496 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 497 | 'provider.iamRoleStatements values shouldn\'t exists'); 498 | assert.isObject( 499 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 500 | 'per function statements imported upon inherit', 501 | ); 502 | }); 503 | 504 | it('role property set to role ARN', () => { 505 | _.set(serverless.service, 'provider.iam', { 506 | role: 'arn:aws:iam::0123456789:role//my/default/path/roleInMyAccount', 507 | }); 508 | 509 | const statements = getLambdaTestStatements(); 510 | 511 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 512 | 'provider.iamRoleStatements values shouldn\'t exists'); 513 | assert.isObject( 514 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 515 | 'per function statements imported upon inherit', 516 | ); 517 | }); 518 | 519 | it('role is set without statements', () => { 520 | _.set(serverless.service, 'provider.iam', { 521 | role: { 522 | managedPolicies: ['arn:aws:iam::123456789012:user/*'], 523 | }, 524 | }); 525 | 526 | const statements = getLambdaTestStatements(); 527 | 528 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 529 | 'provider.iamRoleStatements values shouldn\'t exists'); 530 | assert.isObject( 531 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 532 | 'per function statements imported upon inherit', 533 | ); 534 | }); 535 | 536 | it('empty statements', () => { 537 | _.set(serverless.service, 'provider.iam', { 538 | role: { 539 | statements: [], 540 | }, 541 | }); 542 | 543 | const statements = getLambdaTestStatements(); 544 | 545 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 546 | 'provider.iamRoleStatements values shouldn\'t exists'); 547 | assert.isObject( 548 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 549 | 'per function statements imported upon inherit', 550 | ); 551 | }); 552 | }); 553 | 554 | it('global iam role statements exists in lambda role statements', () => { 555 | _.set(serverless.service, 'provider.iam', { 556 | role: { 557 | statements: [{ 558 | Effect: 'Allow', 559 | Action: [ 560 | 'ec2:CreateNetworkInterface', 561 | ], 562 | Resource: '*', 563 | }], 564 | }, 565 | }); 566 | 567 | const statements = getLambdaTestStatements(); 568 | 569 | assert.isObject( 570 | statements.find((s) => s.Action[0] === 'ec2:CreateNetworkInterface'), 571 | 'global iam role statements exists', 572 | ); 573 | assert.isTrue(statements.find((s) => s.Action[0] === 'xray:PutTelemetryRecords') === undefined, 574 | 'old provider.iamRoleStatements shouldn\'t exists'); 575 | assert.isObject( 576 | statements.find((s) => s.Action[0] === 'dynamodb:GetItem'), 577 | 'per function statements imported upon inherit', 578 | ); 579 | }); 580 | }); 581 | }); 582 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "strict": true, 5 | "noUnusedLocals": true, 6 | "outDir": "./dist", 7 | "declaration": true, 8 | "rootDir": "src", 9 | "target": "es6", 10 | "moduleResolution": "node", 11 | "module": "commonjs", 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "newLine": "LF", 15 | "lib": [ 16 | "es6", 17 | "es2015.promise", 18 | "esnext.asynciterable" 19 | ] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "dist", 24 | ] 25 | } 26 | --------------------------------------------------------------------------------