├── .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 | 
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 |
--------------------------------------------------------------------------------