├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── jest.config.js ├── lambda-packages ├── .no-packagejson-validator ├── identity_provider_handler │ ├── .eslintrc.json │ ├── .gitignore │ ├── .node-version │ ├── LICENSE │ ├── index.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── handler.test.js ├── role_handler │ ├── .eslintrc.json │ ├── .gitignore │ ├── .node-version │ ├── LICENSE │ ├── index.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── handler.test.js └── template.yml ├── lib ├── cfn.ts ├── identityprovider.ts ├── index.ts ├── role.ts └── util.ts ├── package-lock.json ├── package.json ├── test └── irsa.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # SAM artifacts 11 | .aws-sam 12 | samconfig.toml 13 | 14 | # OS artifacts 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | # SAM artifacts 9 | .aws-sam 10 | samconfig.toml 11 | 12 | jest.config.* 13 | .* 14 | *~ 15 | .vscode/ 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": ["lambda-packages/identity_provider_handler", "lambda-packages/role_handler"] 3 | } 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug 4 | report, new feature, correction, or additional documentation, we greatly value 5 | feedback and contributions from our community. 6 | 7 | Please read through this document before submitting any issues or pull requests 8 | to ensure we have all the necessary information to effectively respond to your 9 | bug report or contribution. 10 | 11 | 12 | ## Reporting Bugs/Feature Requests 13 | 14 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 15 | 16 | When filing an issue, please check existing open, or recently closed, issues to 17 | make sure somebody else hasn't already reported the issue. Please try to 18 | include as much information as you can. Details like these are incredibly 19 | useful: 20 | 21 | * A reproducible test case or series of steps 22 | * The version of our code being used 23 | * Any modifications you've made relevant to the bug 24 | * Anything unusual about your environment or deployment 25 | 26 | 27 | ## Contributing via Pull Requests 28 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 29 | 30 | 1. You are working against the latest source on the *master* branch. 31 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 32 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 33 | 34 | To send us a pull request, please: 35 | 36 | 1. Fork the repository. 37 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 38 | 3. Ensure local tests pass. 39 | 4. Commit to your fork using clear commit messages. 40 | 5. Send us a pull request, answering any default questions in the pull request interface. 41 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 42 | 43 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 44 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 45 | 46 | The following rules apply: 47 | 48 | ### Documentation 49 | 50 | Any changes that impact the user experience in any way must be accompanied 51 | by changes to the documentation. 52 | 53 | ### Testing 54 | 55 | Any changes (other than documentation) must be accompanied by tests to be 56 | accepted. Any change to fix an error must be accompanied by a regression test 57 | that ensures the error does not recur in the future. 58 | 59 | To run the test suite, run `npm run test` in the root directory of this 60 | repository. Any new commands needed to run the test suite must be added 61 | to `package.json`. 62 | 63 | 64 | ## Finding contributions to work on 65 | Looking at the existing issues is a great way to find something to contribute 66 | on. As our projects, by default, use the default GitHub issue labels 67 | (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at 68 | any 'help wanted' issues is a great place to start. 69 | 70 | 71 | ## Code of Conduct 72 | This project has adopted the [Amazon Open Source Code of 73 | Conduct](https://aws.github.io/code-of-conduct). For more information see the 74 | [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 75 | opensource-codeofconduct@amazon.com with any additional questions or comments. 76 | 77 | 78 | ## Security issue notifications 79 | If you discover a potential security issue in this project we ask that you 80 | notify AWS/Amazon Security via our [vulnerability reporting 81 | page](http://aws.amazon.com/security/vulnerability-reporting/). Please do 82 | **not** create a public github issue. 83 | 84 | 85 | ## Licensing 86 | 87 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to 88 | confirm the licensing of your contribution. 89 | 90 | We may ask you to sign a [Contributor License Agreement 91 | (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger 92 | changes. 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repository has been archived and will no longer receive updates. The functionality it 3 | > provides is no longer necessary -- please consider using 4 | > [EKS Pod Identities](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) 5 | > instead of IAM Roles for Service Accounts to allow your Pods to obtain AWS IAM credentials. 6 | > Alternatively, you can use CDK's native 7 | > [ServiceAccount](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_eks.ServiceAccount.html) functionality. 8 | > **Use this repository at your own risk as it is no longer being monitored for dependency 9 | > vulnerabilities or other security issues.** 10 | 11 | # Amazon EKS IAM Role for Service Accounts CDK/CloudFormation Library 12 | 13 | This repository contains an [AWS 14 | CloudFormation](https://aws.amazon.com/cloudformation/) [Custom 15 | Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) 16 | that creates an [AWS IAM 17 | Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) that is 18 | assumable by a [Kubernetes Service 19 | Account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/). 20 | This role is known as an IRSA, or [IAM Role for Service 21 | Account](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html). 22 | This role can be associated with an [Amazon EKS](https://aws.amazon.com/eks/) 23 | Cluster that you're creating in the same CloudFormation stack. Alternatively, 24 | the EKS Cluster can be created in a different stack and referenced by name. 25 | 26 | For ease of implementation, this repository also contains a [CDK 27 | Construct](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html) library 28 | you can import and use to easily create a Role. This is the quickest and most 29 | programmatic way to build the Role. 30 | 31 | Alternatively, a SAM Template is available that you can use to deploy the Custom 32 | Resource Lambda Functions to your account and reference in your YAML or JSON 33 | CloudFormation templates. 34 | 35 | ## CDK Construct Library usage 36 | 37 | Install the Construct Library into your TypeScript project as follows: 38 | 39 | ```sh 40 | npm install amazon-eks-irsa-cfn 41 | ``` 42 | 43 | In your source code, import the Construct classes: 44 | 45 | ```typescript 46 | import { Role, OIDCIdentityProvider } from "amazon-eks-irsa-cfn"; 47 | ``` 48 | 49 | Then declare the Constructs in your CDK Stack or Construct. The `Role` class 50 | implements `IRole` and can be used anywhere an `IRole` is needed. 51 | 52 | See also 53 | https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-iam.Role.html for a 54 | list of additional properties that can be supplied when instantiating a `Role`. 55 | 56 | ```typescript 57 | const provider = new OIDCIdentityProvider(this, "Provider", { 58 | clusterName: "MyCluster", 59 | }); 60 | 61 | const role = new Role(this, "Role", { 62 | clusterName: "MyCluster", 63 | serviceAccount: "myServiceAccount", 64 | namespace: "default", 65 | // All other properties available in an `aws-iam.Role` class are available 66 | // e.g. `path`, `maxSessionDuration`, `description`, etc. 67 | }); 68 | ``` 69 | 70 | ## SAM Template and CloudFormation Custom Resources 71 | 72 | There is a SAM Template located in the [`lambda-packages`](lambda-packages/) 73 | folder. It also properly associates the IAM Policies needed for the Lambda 74 | functions to execute properly. 75 | 76 | To deploy it, you can run: 77 | 78 | ``` 79 | sam build 80 | sam deploy 81 | ``` 82 | 83 | The Stack that is created by the Template exports the following values: 84 | 85 | - `EKSIRSARoleCreationFunction` - Role creation Lambda function ARN 86 | - `OIDCIdentityProviderCreationFunction` - OIDC identity provider creation Lambda function ARN 87 | 88 | Once you've deployed the package, you can refer to the Lambda 89 | functions in your CloudFormation Stacks. 90 | 91 | Here's an example Stack fragment that uses these functions to power 92 | Custom Resources: 93 | 94 | ```yaml 95 | Resources: 96 | MyIdentityProvider: 97 | Type: Custom::OIDCIdentityProvider 98 | Properties: 99 | ServiceToken: !ImportValue OIDCIdentityProviderCreationFunction 100 | ClusterName: MyCluster 101 | 102 | MyRole: 103 | Type: Custom::ServiceAccountRole 104 | Properties: 105 | ServiceToken: !ImportValue EKSIRSARoleCreationFunction 106 | ClusterName: MyCluster 107 | ServiceAccount: myServiceAccount 108 | # All other properties supported by AWS::IAM::Role can be 109 | # added here, like Description, Policies, etc. 110 | ``` 111 | 112 | ## License 113 | 114 | This project is licensed under the Apache-2.0 License. 115 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/test" 4 | ], 5 | testMatch: [ '**/*.test.ts'], 6 | "transform": { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /lambda-packages/.no-packagejson-validator: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/amazon-eks-irsa-cfn/34ac9cd1cae0086ae640830a92132ba6a82b1621/lambda-packages/.no-packagejson-validator -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "semi": ["error", "always"] 5 | }, 6 | "env": { 7 | "jest": true, 8 | "node": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | dist 4 | .LAST_PACKAGE 5 | .LAST_BUILD 6 | *.snk 7 | !*.js 8 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/.node-version: -------------------------------------------------------------------------------- 1 | 14.16.0 2 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const aws = require('aws-sdk'); 4 | 5 | const eksOIDCCAThumbprint = exports.eksOIDCCAThumbprint = '9e99a48a9960b14926bb7f3b02e22da2b0ab7280'; 6 | 7 | // These are used for test purposes only 8 | let defaultResponseURL; 9 | 10 | /** 11 | * Upload a CloudFormation response object to S3. 12 | * 13 | * @param {object} event the Lambda event payload received by the handler function 14 | * @param {object} context the Lambda context received by the handler function 15 | * @param {string} responseStatus the response status, either 'SUCCESS' or 'FAILED' 16 | * @param {string} physicalResourceId CloudFormation physical resource ID 17 | * @param {object} [responseData] arbitrary response data object 18 | * @param {string} [reason] reason for failure, if any, to convey to the user 19 | * @returns {Promise} Promise that is resolved on success, or rejected on connection error or HTTP error response 20 | */ 21 | const report = function (event, context, responseStatus, physicalResourceId, responseData, reason) { 22 | return new Promise((resolve, reject) => { 23 | const https = require('https'); 24 | const { 25 | URL 26 | } = require('url'); 27 | 28 | const responseBody = JSON.stringify({ 29 | Status: responseStatus, 30 | Reason: reason, 31 | PhysicalResourceId: physicalResourceId || context.logStreamName, 32 | StackId: event.StackId, 33 | RequestId: event.RequestId, 34 | LogicalResourceId: event.LogicalResourceId, 35 | Data: responseData 36 | }); 37 | 38 | const parsedUrl = new URL(event.ResponseURL || defaultResponseURL); 39 | const options = { 40 | hostname: parsedUrl.hostname, 41 | port: 443, 42 | path: parsedUrl.pathname + parsedUrl.search, 43 | method: 'PUT', 44 | headers: { 45 | 'Content-Type': '', 46 | 'Content-Length': responseBody.length 47 | } 48 | }; 49 | 50 | https.request(options) 51 | .on('error', reject) 52 | .on('response', res => { 53 | res.resume(); 54 | if (res.statusCode >= 400) { 55 | reject(new Error(`Server returned error ${res.statusCode}: ${res.statusMessage}`)); 56 | } else { 57 | resolve(); 58 | } 59 | }) 60 | .end(responseBody, 'utf8'); 61 | }); 62 | }; 63 | 64 | const getIssuerUrl = async function (clusterName) { 65 | const eks = new aws.EKS(); 66 | const { 67 | cluster 68 | } = await eks.describeCluster({ 69 | name: clusterName 70 | }).promise(); 71 | return cluster.identity.oidc.issuer; 72 | }; 73 | 74 | const createProvider = async function (url) { 75 | const iam = new aws.IAM(); 76 | 77 | console.log('Creating identity provider...'); 78 | const provider = await iam.createOpenIDConnectProvider({ 79 | Url: url, 80 | // hard-coding this for now to be safe - this is the certificate 81 | // thumbprint for the EKS OIDC endpoint CA 82 | ThumbprintList: [eksOIDCCAThumbprint], 83 | ClientIDList: ['sts.amazonaws.com'] 84 | }).promise(); 85 | return provider.OpenIDConnectProviderArn; 86 | }; 87 | 88 | const deleteProvider = async function (providerArn) { 89 | const iam = new aws.IAM(); 90 | 91 | console.log(`Deleting provider ${providerArn}...`); 92 | try { 93 | await iam.deleteOpenIDConnectProvider({ 94 | OpenIDConnectProviderArn: providerArn 95 | }).promise(); 96 | } catch (err) { 97 | console.error(err); 98 | // if (err.name !== 'ResourceNotFoundException') { 99 | // throw err; 100 | // } 101 | } 102 | }; 103 | 104 | /** 105 | * Main handler, invoked by Lambda 106 | */ 107 | exports.handler = async function (event, context) { 108 | const responseData = {}; 109 | let physicalResourceId; 110 | let issuerUrl; 111 | 112 | if (process.stdout._handle) process.stdout._handle.setBlocking(true); 113 | if (process.stderr._handle) process.stderr._handle.setBlocking(true); 114 | 115 | try { 116 | switch (event.RequestType) { 117 | case 'Update': 118 | await deleteProvider(event.PhysicalResourceId); 119 | // no break here - fallthrough to create 120 | case 'Create': 121 | issuerUrl = await getIssuerUrl(event.ResourceProperties.ClusterName); 122 | responseData.Arn = physicalResourceId = await createProvider(issuerUrl); 123 | break; 124 | case 'Delete': 125 | physicalResourceId = event.PhysicalResourceId; 126 | await deleteProvider(physicalResourceId); 127 | break; 128 | default: 129 | throw new Error(`Unsupported request type ${event.RequestType}`); 130 | } 131 | 132 | console.log('Uploading SUCCESS response to S3...'); 133 | await report(event, context, 'SUCCESS', physicalResourceId, responseData); 134 | } catch (err) { 135 | console.error(`Caught error ${err}. Uploading FAILED message to S3.`); 136 | await report(event, context, 'FAILED', physicalResourceId, null, err.message); 137 | } 138 | }; 139 | 140 | exports.withDefaultResponseURL = function (url) { 141 | defaultResponseURL = url; 142 | }; 143 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/test" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | "testRegex": "(/test/.*|(\\.|/)(test|spec))\\.(ts|js)x?$", 9 | "moduleFileExtensions": [ 10 | "ts", 11 | "tsx", 12 | "js", 13 | "jsx", 14 | "json", 15 | "node" 16 | ], 17 | "testEnvironment": "node" 18 | } 19 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "identity_provider_handler", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "author": "Michael S. Fischer", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "aws-sdk": "^2.895.0" 13 | }, 14 | "devDependencies": { 15 | "aws-sdk-mock": "^5.1.0", 16 | "eslint": "^7.25.0", 17 | "jest": "^29.3.1", 18 | "lambda-tester": "^4.0.1", 19 | "nock": "^13.0.11", 20 | "standard": "^16.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lambda-packages/identity_provider_handler/test/handler.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk-mock'); 4 | const LambdaTester = require('lambda-tester').noVersionCheck(); 5 | const sinon = require('sinon'); 6 | const handler = require('..'); 7 | const nock = require('nock'); 8 | const ResponseURL = 'https://iam-response-mock.example.com/'; 9 | 10 | AWS.setSDK(require.resolve('aws-sdk')); 11 | 12 | describe('OIDC Identity Provider Handler', () => { 13 | const origLog = console.log; 14 | const origErr = console.error; 15 | const testRequestId = 'f4ef1b10-c39a-44e3-99c0-fbf7e53c3943'; 16 | const testClusterName = 'test'; 17 | const testNewClusterName = 'test2'; 18 | const testClusterOIDCIssuerURL = 'https://oidc.eks.us-east-1.amazonaws.com/id/EBAABEEF'; 19 | const testNewClusterOIDCIssuerURL = 'https://oidc.eks.us-east-1.amazonaws.com/id/ABCDEF'; 20 | const testOIDCProviderArn = 'arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EBAABEEF'; 21 | const testNewOIDCProviderArn = 'arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/ABCDEF'; 22 | 23 | beforeEach(() => { 24 | handler.withDefaultResponseURL(ResponseURL); 25 | console.log = console.error = function () { }; 26 | }); 27 | afterEach(() => { 28 | // Restore waiters and logger 29 | AWS.restore(); 30 | console.log = origLog; 31 | console.error = origErr; 32 | }); 33 | 34 | test('Empty event payload fails', () => { 35 | const request = nock(ResponseURL).put('/', body => { 36 | return body.Status === 'FAILED' && body.Reason === 'Unsupported request type undefined'; 37 | }).reply(200); 38 | return LambdaTester(handler.handler) 39 | .event({}) 40 | .expectResolve(() => { 41 | expect(request.isDone()).toBe(true); 42 | }); 43 | }); 44 | 45 | test('Bogus operation fails', () => { 46 | const bogusType = 'bogus'; 47 | const request = nock(ResponseURL).put('/', body => { 48 | return body.Status === 'FAILED' && body.Reason === 'Unsupported request type ' + bogusType; 49 | }).reply(200); 50 | return LambdaTester(handler.handler) 51 | .event({ 52 | RequestType: bogusType 53 | }) 54 | .expectResolve(() => { 55 | expect(request.isDone()).toBe(true); 56 | }); 57 | }); 58 | 59 | test('Create operation creates an OIDC Provider', () => { 60 | const describeClusterFake = sinon.fake.resolves({ 61 | cluster: { 62 | identity: { 63 | oidc: { 64 | issuer: testClusterOIDCIssuerURL 65 | } 66 | } 67 | } 68 | }); 69 | 70 | const createProviderFake = sinon.fake.resolves({ 71 | OpenIDConnectProviderArn: testOIDCProviderArn 72 | }); 73 | 74 | AWS.mock('EKS', 'describeCluster', describeClusterFake); 75 | AWS.mock('IAM', 'createOpenIDConnectProvider', createProviderFake); 76 | 77 | const request = nock(ResponseURL).put('/', body => { 78 | return body.Status === 'SUCCESS'; 79 | }).reply(200); 80 | 81 | return LambdaTester(handler.handler) 82 | .event({ 83 | RequestType: 'Create', 84 | RequestId: testRequestId, 85 | ResourceProperties: { 86 | ClusterName: testClusterName 87 | } 88 | }).expectResolve(() => { 89 | sinon.assert.calledWith(describeClusterFake, sinon.match({ 90 | name: testClusterName 91 | })); 92 | sinon.assert.calledWith(createProviderFake, sinon.match({ 93 | Url: testClusterOIDCIssuerURL, 94 | ThumbprintList: [handler.eksOIDCCAThumbprint], 95 | ClientIDList: ['sts.amazonaws.com'] 96 | })); 97 | expect(request.isDone()).toBe(true); 98 | }); 99 | }); 100 | 101 | test('Delete operation deletes the OIDC Provider', () => { 102 | const deleteProviderFake = sinon.fake.resolves(); 103 | 104 | AWS.mock('IAM', 'deleteOpenIDConnectProvider', deleteProviderFake); 105 | 106 | const request = nock(ResponseURL).put('/', body => { 107 | return body.Status === 'SUCCESS'; 108 | }).reply(200); 109 | 110 | return LambdaTester(handler.handler) 111 | .event({ 112 | RequestType: 'Delete', 113 | RequestId: testRequestId, 114 | PhysicalResourceId: testOIDCProviderArn 115 | }).expectResolve(() => { 116 | sinon.assert.calledWith(deleteProviderFake, sinon.match({ 117 | OpenIDConnectProviderArn: testOIDCProviderArn 118 | })); 119 | expect(request.isDone()).toBe(true); 120 | }); 121 | }); 122 | 123 | test('Update operation replaces the OIDC Provider', () => { 124 | const describeClusterFake = sinon.fake.resolves({ 125 | cluster: { 126 | identity: { 127 | oidc: { 128 | issuer: testNewClusterOIDCIssuerURL 129 | } 130 | } 131 | } 132 | }); 133 | 134 | const createProviderFake = sinon.fake.resolves({ 135 | OpenIDConnectProviderArn: testNewOIDCProviderArn 136 | }); 137 | 138 | AWS.mock('EKS', 'describeCluster', describeClusterFake); 139 | AWS.mock('IAM', 'createOpenIDConnectProvider', createProviderFake); 140 | 141 | const deleteProviderFake = sinon.fake.resolves(); 142 | 143 | AWS.mock('IAM', 'deleteOpenIDConnectProvider', deleteProviderFake); 144 | 145 | const request = nock(ResponseURL).put('/', body => { 146 | return body.Status === 'SUCCESS'; 147 | }).reply(200); 148 | 149 | return LambdaTester(handler.handler) 150 | .event({ 151 | RequestType: 'Update', 152 | RequestId: testRequestId, 153 | PhysicalResourceId: testOIDCProviderArn, 154 | ResourceProperties: { 155 | ClusterName: testNewClusterName 156 | } 157 | }).expectResolve(() => { 158 | sinon.assert.calledWith(deleteProviderFake, sinon.match({ 159 | OpenIDConnectProviderArn: testOIDCProviderArn 160 | })); 161 | sinon.assert.calledWith(describeClusterFake, sinon.match({ 162 | name: testNewClusterName 163 | })); 164 | sinon.assert.calledWith(createProviderFake, sinon.match({ 165 | Url: testNewClusterOIDCIssuerURL, 166 | ThumbprintList: [handler.eksOIDCCAThumbprint], 167 | ClientIDList: ['sts.amazonaws.com'] 168 | })); 169 | expect(request.isDone()).toBe(true); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "semi": ["error", "always"] 5 | }, 6 | "env": { 7 | "jest": true, 8 | "node": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | dist 4 | .LAST_PACKAGE 5 | .LAST_BUILD 6 | *.snk 7 | !*.js 8 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/.node-version: -------------------------------------------------------------------------------- 1 | 14.16.0 2 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const aws = require('aws-sdk'); 4 | 5 | const maxRoleNameLen = 63; 6 | const randomSuffixLen = 12; 7 | const maxTruncatedRoleNameLen = maxRoleNameLen - randomSuffixLen - 1; 8 | const suffixChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 9 | 10 | // These are used for test purposes only 11 | let defaultResponseURL; 12 | let waiter; 13 | 14 | /** 15 | * Upload a CloudFormation response object to S3. 16 | * 17 | * @param {object} event the Lambda event payload received by the handler function 18 | * @param {object} context the Lambda context received by the handler function 19 | * @param {string} responseStatus the response status, either 'SUCCESS' or 'FAILED' 20 | * @param {string} physicalResourceId CloudFormation physical resource ID 21 | * @param {object} [responseData] arbitrary response data object 22 | * @param {string} [reason] reason for failure, if any, to convey to the user 23 | * @returns {Promise} Promise that is resolved on success, or rejected on connection error or HTTP error response 24 | */ 25 | const report = function (event, context, responseStatus, physicalResourceId, responseData, reason) { 26 | return new Promise((resolve, reject) => { 27 | const https = require('https'); 28 | const { 29 | URL 30 | } = require('url'); 31 | 32 | const responseBody = JSON.stringify({ 33 | Status: responseStatus, 34 | Reason: reason, 35 | PhysicalResourceId: physicalResourceId || context.logStreamName, 36 | StackId: event.StackId, 37 | RequestId: event.RequestId, 38 | LogicalResourceId: event.LogicalResourceId, 39 | Data: responseData 40 | }); 41 | 42 | const parsedUrl = new URL(event.ResponseURL || defaultResponseURL); 43 | const options = { 44 | hostname: parsedUrl.hostname, 45 | port: 443, 46 | path: parsedUrl.pathname + parsedUrl.search, 47 | method: 'PUT', 48 | headers: { 49 | 'Content-Type': '', 50 | 'Content-Length': responseBody.length 51 | } 52 | }; 53 | 54 | https.request(options) 55 | .on('error', reject) 56 | .on('response', res => { 57 | res.resume(); 58 | if (res.statusCode >= 400) { 59 | reject(new Error(`Server returned error ${res.statusCode}: ${res.statusMessage}`)); 60 | } else { 61 | resolve(); 62 | } 63 | }) 64 | .end(responseBody, 'utf8'); 65 | }); 66 | }; 67 | 68 | const getAccountId = async function () { 69 | const sts = new aws.STS(); 70 | const resp = await sts.getCallerIdentity().promise(); 71 | return resp.Account; 72 | }; 73 | 74 | const getIssuer = async function (clusterName) { 75 | const eks = new aws.EKS(); 76 | const { 77 | cluster 78 | } = await eks.describeCluster({ 79 | name: clusterName 80 | }).promise(); 81 | return cluster.identity.oidc.issuer.replace(new RegExp('^https?://'), ''); 82 | }; 83 | 84 | const getAssumeRolePolicy = function (accountId, issuer, namespace, serviceAccount) { 85 | return JSON.stringify({ 86 | Version: '2012-10-17', 87 | Statement: [{ 88 | Effect: 'Allow', 89 | Principal: { 90 | Federated: `arn:aws:iam::${accountId}:oidc-provider/${issuer}` 91 | }, 92 | Action: 'sts:AssumeRoleWithWebIdentity', 93 | Condition: { 94 | StringEquals: { 95 | [`${issuer}:sub`]: `system:serviceaccount:${namespace}:${serviceAccount}`, 96 | [`${issuer}:aud`]: 'sts.amazonaws.com' 97 | } 98 | } 99 | }] 100 | }); 101 | }; 102 | 103 | const createRole = async function (props) { 104 | const iam = new aws.IAM(); 105 | 106 | if (waiter) { 107 | // Used by the test suite, since waiters aren't mockable yet 108 | iam.waitFor = waiter; 109 | } 110 | 111 | console.log('Creating role...'); 112 | const { 113 | Role 114 | } = await iam.createRole({ 115 | AssumeRolePolicyDocument: getAssumeRolePolicy( 116 | await getAccountId(), 117 | await getIssuer(props.ClusterName), 118 | props.Namespace, 119 | props.ServiceAccount 120 | ), 121 | RoleName: props.RoleName, 122 | Description: props.Description, 123 | MaxSessionDuration: props.MaxSessionDuration, 124 | Path: props.Path, 125 | PermissionsBoundary: props.PermissionsBoundary 126 | // Tags: ...? 127 | }).promise(); 128 | 129 | console.log('Waiting for IAM role creation to finalize...'); 130 | await iam.waitFor('roleExists', { 131 | RoleName: Role.RoleName 132 | }).promise(); 133 | 134 | console.log('Attaching role policies...'); 135 | for (const policy of props.Policies || []) { 136 | await iam.putRolePolicy({ 137 | RoleName: Role.RoleName, 138 | PolicyName: policy.PolicyName, 139 | PolicyDocument: policy.PolicyDocument 140 | }).promise(); 141 | } 142 | for (const arn of props.ManagedPolicyArns || []) { 143 | await iam.attachRolePolicy({ 144 | RoleName: Role.RoleName, 145 | PolicyArn: arn 146 | }).promise(); 147 | } 148 | return Role; 149 | }; 150 | 151 | const generateRoleName = exports.generateRoleName = function (logicalResourceId) { 152 | let roleName = logicalResourceId.substr(0, maxTruncatedRoleNameLen) + '-'; 153 | for (let i = 0; i < randomSuffixLen; i++) { 154 | roleName = roleName + suffixChars[Math.floor(Math.random() * suffixChars.length)]; 155 | } 156 | return roleName; 157 | }; 158 | 159 | const updateRole = async function (roleName, props, oldProps) { 160 | const iam = new aws.IAM(); 161 | 162 | if (waiter) { 163 | // Used by the test suite, since waiters aren't mockable yet 164 | iam.waitFor = waiter; 165 | } 166 | 167 | console.log(`Updating role ${roleName}...`); 168 | 169 | const { 170 | Role 171 | } = await iam.getRole({ 172 | RoleName: props.RoleName 173 | }).promise(); 174 | 175 | let updateRoleProps = {}; 176 | if (props.Description !== undefined) { 177 | updateRoleProps.Description = props.Description; 178 | } 179 | if (props.MaxSessionDuration !== undefined) { 180 | updateRoleProps.MaxSessionDuration = props.MaxSessionDuration; 181 | } 182 | if (updateRoleProps !== undefined) { 183 | updateRoleProps.RoleName = roleName; 184 | await iam.updateRole(updateRoleProps).promise(); 185 | } 186 | 187 | for (const policy of oldProps.Policies || []) { 188 | if (props.Policies === undefined || !props.Policies.map(x => x.PolicyName).includes(policy.PolicyName)) { 189 | console.log(`Delete role policy ${policy.PolicyName}...`); 190 | await iam.deleteRolePolicy({ 191 | RoleName: roleName, 192 | PolicyName: policy.PolicyName 193 | }).promise(); 194 | } 195 | } 196 | for (const policy of props.Policies || []) { 197 | console.log(`Add role policy ${policy.PolicyName}...`); 198 | await iam.putRolePolicy({ 199 | RoleName: roleName, 200 | PolicyName: policy.PolicyName, 201 | PolicyDocument: policy.PolicyDocument 202 | }).promise(); 203 | } 204 | 205 | for (const arn of oldProps.ManagedPolicyArns || []) { 206 | if (props.ManagedPolicyArns === undefined || !props.ManagedPolicyArns.includes(arn)) { 207 | console.log(`Detach role policy ${arn}...`); 208 | try { 209 | await iam.detachRolePolicy({ 210 | RoleName: roleName, 211 | PolicyArn: arn 212 | }).promise(); 213 | } catch (err) { 214 | if (err.name !== 'NoSuchEntity') { 215 | throw err; 216 | } 217 | } 218 | } 219 | } 220 | for (const arn of props.ManagedPolicyArns || []) { 221 | if (oldProps.ManagedPolicyArns === undefined || !oldProps.ManagedPolicyArns.includes(arn)) { 222 | console.log(`Attach role policy ${arn}...`); 223 | await iam.attachRolePolicy({ 224 | RoleName: roleName, 225 | PolicyArn: arn 226 | }).promise(); 227 | } 228 | } 229 | return Role; 230 | }; 231 | 232 | const deleteRole = async function (roleName, props) { 233 | const iam = new aws.IAM(); 234 | 235 | console.log(`Deleting role ${roleName}...`); 236 | try { 237 | for (const arn of props.ManagedPolicyArns || []) { 238 | await iam.detachRolePolicy({ 239 | RoleName: roleName, 240 | PolicyArn: arn 241 | }).promise(); 242 | } 243 | await iam.deleteRole({ 244 | RoleName: roleName 245 | }).promise(); 246 | } catch (err) { 247 | if (err.name !== 'ResourceNotFoundException') { 248 | throw err; 249 | } 250 | } 251 | }; 252 | 253 | /** 254 | * Main handler, invoked by Lambda 255 | */ 256 | exports.handler = async function (event, context) { 257 | const responseData = {}; 258 | let physicalResourceId; 259 | let role; 260 | 261 | if (process.stdout._handle) process.stdout._handle.setBlocking(true); 262 | if (process.stderr._handle) process.stderr._handle.setBlocking(true); 263 | 264 | console.log(JSON.stringify(event)) 265 | 266 | try { 267 | switch (event.RequestType) { 268 | case 'Create': 269 | if (!event.ResourceProperties.RoleName) { 270 | event.ResourceProperties.RoleName = generateRoleName(event.LogicalResourceId); 271 | } 272 | role = await createRole(event.ResourceProperties); 273 | responseData.Arn = role.Arn; 274 | responseData.RoleId = role.RoleId; 275 | physicalResourceId = role.RoleName; 276 | break; 277 | case 'Update': 278 | physicalResourceId = event.PhysicalResourceId; 279 | role = await updateRole(physicalResourceId, event.ResourceProperties, event.OldResourceProperties); 280 | responseData.Arn = role.Arn; 281 | responseData.RoleId = role.RoleId; 282 | physicalResourceId = role.RoleName; 283 | break; 284 | case 'Delete': 285 | physicalResourceId = event.PhysicalResourceId; 286 | await deleteRole(physicalResourceId, event.ResourceProperties); 287 | break; 288 | default: 289 | throw new Error(`Unsupported request type ${event.RequestType}`); 290 | } 291 | 292 | console.log('Uploading SUCCESS response to S3...'); 293 | await report(event, context, 'SUCCESS', physicalResourceId, responseData); 294 | } catch (err) { 295 | console.error(`Caught error ${err}. Uploading FAILED message to S3.`); 296 | await report(event, context, 'FAILED', physicalResourceId, null, err.message); 297 | } 298 | }; 299 | 300 | /** 301 | * @private 302 | */ 303 | exports.withDefaultResponseURL = function (url) { 304 | defaultResponseURL = url; 305 | }; 306 | 307 | /** 308 | * @private 309 | */ 310 | exports.withWaiter = function (w) { 311 | waiter = w; 312 | }; 313 | 314 | /** 315 | * @private 316 | */ 317 | exports.resetWaiter = function () { 318 | waiter = undefined; 319 | }; 320 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [ 3 | '/test' 4 | ], 5 | transform: { 6 | // '^.+\\.tsx?$': 'ts-jest' 7 | }, 8 | testRegex: '(/test/.*|(\\.|/)(test|spec))\\.(ts|js)x?$', 9 | moduleFileExtensions: [ 10 | 'ts', 11 | 'tsx', 12 | 'js', 13 | 'jsx', 14 | 'json', 15 | 'node' 16 | ], 17 | testEnvironment: 'node' 18 | } 19 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "role_handler", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "author": "Michael S. Fischer", 10 | "license": "Apache-2.0", 11 | "dependencies": { 12 | "aws-sdk": "^2.895.0" 13 | }, 14 | "devDependencies": { 15 | "aws-sdk-mock": "^5.1.0", 16 | "eslint": "^7.25.0", 17 | "jest": "^29.3.1", 18 | "lambda-tester": "^4.0.1", 19 | "nock": "^13.0.11", 20 | "standard": "^16.0.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lambda-packages/role_handler/test/handler.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk-mock'); 4 | const LambdaTester = require('lambda-tester').noVersionCheck(); 5 | const sinon = require('sinon'); 6 | const handler = require('..'); 7 | const nock = require('nock'); 8 | const ResponseURL = 'https://iam-response-mock.example.com/'; 9 | const maxRoleNameLen = 63; 10 | 11 | AWS.setSDK(require.resolve('aws-sdk')); 12 | 13 | describe('IAM Role Resource Handler', () => { 14 | const origLog = console.log; 15 | const origErr = console.error; 16 | const testLogicalResourceId = 'TestRoleResource'; 17 | const testDescription = 'This is my role'; 18 | const testRequestId = 'f4ef1b10-c39a-44e3-99c0-fbf7e53c3943'; 19 | const testClusterName = 'testCluster'; 20 | const testRoleName = 'testRole'; 21 | const testMaxSessionDuration = 300; 22 | const testPath = '/'; 23 | const testAccountId = '123456789012'; 24 | const testPermissionsBoundary = 'arn:aws:iam::aws:policy/ReadOnlyAccess'; 25 | const testPolicy = { 26 | PolicyName: 'Test Policy', 27 | PolicyDocument: JSON.stringify({ 28 | Version: '2012-10-17', 29 | Statement: [{ 30 | Effect: 'Allow', 31 | Action: '*', 32 | Resource: '*' 33 | }] 34 | }) 35 | }; 36 | const testPolicy2 = { 37 | PolicyName: 'Test Policy 2', 38 | PolicyDocument: testPolicy.PolicyDocument 39 | }; 40 | const testManagedPolicyArn = 'arn:aws:iam::aws:policy/AdministratorAccess'; 41 | const testManagedPolicyArn2 = 'arn:aws:iam::aws:policy/AmazonS3FullAccess'; 42 | const testClusterOIDCIssuerURL = 'https://oidc.eks.us-east-1.amazonaws.com/id/EBAABEEF'; 43 | const testIssuer = testClusterOIDCIssuerURL.replace(new RegExp('^https?://'), ''); 44 | const testServiceAccount = 'testServiceAccount'; 45 | const testNamespace = 'testNamespace'; 46 | 47 | beforeEach(() => { 48 | handler.withDefaultResponseURL(ResponseURL); 49 | handler.withWaiter(function () { 50 | // Mock waiter is merely a self-fulfilling promise 51 | return { 52 | promise: () => { 53 | return new Promise((resolve) => { 54 | resolve(); 55 | }); 56 | } 57 | }; 58 | }); 59 | // handler.withSleep(spySleep); 60 | console.log = console.error = function () {}; 61 | }); 62 | afterEach(() => { 63 | // Restore waiters and logger 64 | AWS.restore(); 65 | handler.resetWaiter(); 66 | console.log = origLog; 67 | console.error = origErr; 68 | }); 69 | 70 | test('Empty event payload fails', () => { 71 | const request = nock(ResponseURL).put('/', body => { 72 | return body.Status === 'FAILED' && body.Reason === 'Unsupported request type undefined'; 73 | }).reply(200); 74 | return LambdaTester(handler.handler) 75 | .event({}) 76 | .expectResolve(() => { 77 | expect(request.isDone()).toBe(true); 78 | }); 79 | }); 80 | 81 | test('Bogus operation fails', () => { 82 | const bogusType = 'bogus'; 83 | const request = nock(ResponseURL).put('/', body => { 84 | return body.Status === 'FAILED' && body.Reason === 'Unsupported request type ' + bogusType; 85 | }).reply(200); 86 | return LambdaTester(handler.handler) 87 | .event({ 88 | RequestType: bogusType 89 | }) 90 | .expectResolve(() => { 91 | expect(request.isDone()).toBe(true); 92 | }); 93 | }); 94 | 95 | test('Role name generator', () => { 96 | const roleName = handler.generateRoleName(testLogicalResourceId); 97 | expect(roleName.length).toBeLessThanOrEqual(maxRoleNameLen); 98 | expect(roleName).toMatch(/^[A-Za-z]+-[A-Z0-9]+$/); 99 | }); 100 | 101 | test('Create operation creates a new Role', () => { 102 | const createRoleFake = sinon.fake.resolves({ 103 | Role: { 104 | RoleName: testRoleName 105 | } 106 | }); 107 | const getRoleFake = sinon.stub(); 108 | getRoleFake.onFirstCall().rejects(); 109 | getRoleFake.resolves(); 110 | const putRolePolicyFake = sinon.fake.resolves(); 111 | const attachRolePolicyFake = sinon.fake.resolves(); 112 | const getCallerIdentityFake = sinon.fake.resolves({ 113 | Account: testAccountId 114 | }); 115 | const describeClusterFake = sinon.fake.resolves({ 116 | cluster: { 117 | identity: { 118 | oidc: { 119 | issuer: testClusterOIDCIssuerURL 120 | } 121 | } 122 | } 123 | }); 124 | 125 | AWS.mock('IAM', 'createRole', createRoleFake); 126 | AWS.mock('IAM', 'putRolePolicy', putRolePolicyFake); 127 | AWS.mock('IAM', 'attachRolePolicy', attachRolePolicyFake); 128 | AWS.mock('STS', 'getCallerIdentity', getCallerIdentityFake); 129 | AWS.mock('EKS', 'describeCluster', describeClusterFake); 130 | 131 | const request = nock(ResponseURL).put('/', body => { 132 | return body.Status === 'SUCCESS'; 133 | }).reply(200); 134 | 135 | return LambdaTester(handler.handler) 136 | .event({ 137 | RequestType: 'Create', 138 | RequestId: testRequestId, 139 | LogicalResourceId: testLogicalResourceId, 140 | ResourceProperties: { 141 | ClusterName: testClusterName, 142 | RoleName: testRoleName, 143 | Description: testDescription, 144 | MaxSessionDuration: testMaxSessionDuration, 145 | Path: testPath, 146 | PermissionsBoundary: testPermissionsBoundary, 147 | Policies: [testPolicy], 148 | ManagedPolicyArns: [testManagedPolicyArn], 149 | Namespace: testNamespace, 150 | ServiceAccount: testServiceAccount 151 | } 152 | }).expectResolve(() => { 153 | sinon.assert.called(getCallerIdentityFake); 154 | sinon.assert.calledWith(describeClusterFake, { 155 | name: testClusterName 156 | }); 157 | sinon.assert.calledWith(createRoleFake, sinon.match({ 158 | AssumeRolePolicyDocument: JSON.stringify({ 159 | Version: '2012-10-17', 160 | Statement: [{ 161 | Effect: 'Allow', 162 | Principal: { 163 | Federated: `arn:aws:iam::${testAccountId}:oidc-provider/${testIssuer}` 164 | }, 165 | Action: 'sts:AssumeRoleWithWebIdentity', 166 | Condition: { 167 | StringEquals: { 168 | [`${testIssuer}:sub`]: `system:serviceaccount:${testNamespace}:${testServiceAccount}`, 169 | [`${testIssuer}:aud`]: 'sts.amazonaws.com' 170 | } 171 | } 172 | }] 173 | }), 174 | RoleName: testRoleName, 175 | Description: testDescription, 176 | MaxSessionDuration: testMaxSessionDuration, 177 | Path: testPath, 178 | PermissionsBoundary: testPermissionsBoundary 179 | })); 180 | sinon.assert.calledWith(putRolePolicyFake, sinon.match({ 181 | RoleName: testRoleName, 182 | PolicyName: testPolicy.PolicyName, 183 | PolicyDocument: testPolicy.PolicyDocument 184 | })); 185 | sinon.assert.calledWith(attachRolePolicyFake, sinon.match({ 186 | RoleName: testRoleName, 187 | PolicyArn: testManagedPolicyArn, 188 | })); 189 | expect(request.isDone()).toBe(true); 190 | }); 191 | }); 192 | 193 | test('Update operation updates an existing Role', () => { 194 | const updateRoleFake = sinon.fake.resolves(); 195 | const getRoleFake = sinon.fake.resolves({ 196 | Role: { 197 | RoleName: testRoleName 198 | } 199 | }); 200 | const putRolePolicyFake = sinon.fake.resolves(); 201 | const deleteRolePolicyFake = sinon.fake.resolves(); 202 | const attachRolePolicyFake = sinon.fake.resolves(); 203 | const detachRolePolicyFake = sinon.fake.resolves(); 204 | 205 | AWS.mock('IAM', 'updateRole', updateRoleFake); 206 | AWS.mock('IAM', 'getRole', getRoleFake); 207 | AWS.mock('IAM', 'putRolePolicy', putRolePolicyFake); 208 | AWS.mock('IAM', 'deleteRolePolicy', deleteRolePolicyFake); 209 | AWS.mock('IAM', 'attachRolePolicy', attachRolePolicyFake); 210 | AWS.mock('IAM', 'detachRolePolicy', detachRolePolicyFake); 211 | 212 | const request = nock(ResponseURL).put('/', body => { 213 | return body.Status === 'SUCCESS'; 214 | }).reply(200); 215 | 216 | return LambdaTester(handler.handler) 217 | .event({ 218 | RequestType: 'Update', 219 | RequestId: testRequestId, 220 | PhysicalResourceId: testRoleName, 221 | ResourceProperties: { 222 | ClusterName: testClusterName, 223 | RoleName: testRoleName, 224 | Description: testDescription, 225 | MaxSessionDuration: testMaxSessionDuration, 226 | Policies: [testPolicy2], 227 | ManagedPolicyArns: [testManagedPolicyArn2], 228 | Namespace: testNamespace, 229 | ServiceAccount: testServiceAccount 230 | }, 231 | OldResourceProperties: { 232 | RoleName: testRoleName, 233 | Policies: [testPolicy], 234 | ManagedPolicyArns: [testManagedPolicyArn], 235 | } 236 | }).expectResolve(() => { 237 | sinon.assert.calledWith(getRoleFake, { 238 | RoleName: testRoleName 239 | }); 240 | sinon.assert.calledWith(updateRoleFake, { 241 | RoleName: testRoleName, 242 | MaxSessionDuration: testMaxSessionDuration, 243 | Description: testDescription 244 | }); 245 | sinon.assert.calledWith(deleteRolePolicyFake, sinon.match({ 246 | RoleName: testRoleName, 247 | PolicyName: testPolicy.PolicyName 248 | })); 249 | sinon.assert.calledWith(putRolePolicyFake, sinon.match({ 250 | RoleName: testRoleName, 251 | PolicyName: testPolicy2.PolicyName, 252 | PolicyDocument: testPolicy2.PolicyDocument 253 | })); 254 | sinon.assert.calledWith(detachRolePolicyFake, sinon.match({ 255 | RoleName: testRoleName, 256 | PolicyArn: testManagedPolicyArn, 257 | })); 258 | sinon.assert.calledWith(attachRolePolicyFake, sinon.match({ 259 | RoleName: testRoleName, 260 | PolicyArn: testManagedPolicyArn2, 261 | })); 262 | expect(request.isDone()).toBe(true); 263 | }); 264 | }); 265 | 266 | test('Delete operation deletes the IAM Role', () => { 267 | const deleteRoleFake = sinon.fake.resolves(); 268 | const detachRolePolicyFake = sinon.fake.resolves(); 269 | 270 | AWS.mock('IAM', 'deleteRole', deleteRoleFake); 271 | AWS.mock('IAM', 'detachRolePolicy', detachRolePolicyFake); 272 | 273 | const request = nock(ResponseURL).put('/', body => { 274 | return body.Status === 'SUCCESS'; 275 | }).reply(200); 276 | 277 | return LambdaTester(handler.handler) 278 | .event({ 279 | RequestType: 'Delete', 280 | RequestId: testRequestId, 281 | PhysicalResourceId: testRoleName, 282 | ResourceProperties: { 283 | ManagedPolicyArns: [testManagedPolicyArn], 284 | } 285 | }).expectResolve(() => { 286 | sinon.assert.calledWith(deleteRoleFake, sinon.match({ 287 | RoleName: testRoleName 288 | })); 289 | sinon.assert.calledWith(detachRolePolicyFake, sinon.match({ 290 | RoleName: testRoleName, 291 | PolicyArn: testManagedPolicyArn 292 | })); 293 | expect(request.isDone()).toBe(true); 294 | }); 295 | }); 296 | }); 297 | -------------------------------------------------------------------------------- /lambda-packages/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | 4 | Globals: 5 | Function: 6 | Runtime: nodejs12.x 7 | Timeout: 900 8 | Handler: index.handler 9 | 10 | Description: CloudFormation Custom Resources to create EKS IAM Role for Service Account 11 | 12 | Resources: 13 | RoleCreationFunction: 14 | Type: AWS::Serverless::Function 15 | Properties: 16 | CodeUri: ./role_handler 17 | Policies: 18 | - Statement: 19 | - Sid: Default 20 | Effect: Allow 21 | Action: 22 | - eks:DescribeCluster 23 | - iam:AttachRolePolicy 24 | - iam:CreateRole 25 | - iam:DeleteRole 26 | - iam:DeleteRolePolicy 27 | - iam:DescribeRole 28 | - iam:DetachRolePolicy 29 | - iam:GetRole 30 | - iam:ListAttachedRolePolicies 31 | - iam:ListRoles 32 | - iam:PutRolePermissionsBoundary 33 | - iam:PutRolePolicy 34 | - iam:TagRole 35 | - iam:UntagRole 36 | - iam:UpdateAssumeRolePolicy 37 | - iam:UpdateRole 38 | - sts:GetCallerIdentity 39 | Resource: '*' 40 | 41 | OIDCIdentityProviderCreationFunction: 42 | Type: AWS::Serverless::Function 43 | Properties: 44 | CodeUri: ./identity_provider_handler 45 | Policies: 46 | - Statement: 47 | - Sid: Default 48 | Effect: Allow 49 | Action: 50 | - eks:DescribeCluster 51 | - iam:CreateOpenIDConnectProvider 52 | - iam:DeleteOpenIDConnectProvider 53 | Resource: '*' 54 | 55 | Outputs: 56 | RoleCreationFunction: 57 | Description: Role creation Lambda function ARN 58 | Value: !GetAtt RoleCreationFunction.Arn 59 | Export: 60 | Name: EKSIRSARoleCreationFunction 61 | OIDCIdentityProviderCreationFunction: 62 | Description: OIDC identity provider creation Lambda function ARN 63 | Value: !GetAtt OIDCIdentityProviderCreationFunction.Arn 64 | Export: 65 | Name: EKSIRSAOIDCIdentityProviderCreationFunction 66 | -------------------------------------------------------------------------------- /lib/cfn.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from '@aws-cdk/core'; 2 | 3 | // Borrowed from CfnRole.PolicyProperty 4 | export namespace CfnRole { 5 | /** 6 | * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html 7 | */ 8 | export interface PolicyProperty { 9 | /** 10 | * `CfnRole.PolicyProperty.PolicyDocument` 11 | * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policydocument 12 | */ 13 | readonly policyDocument: object | cdk.Token; 14 | /** 15 | * `CfnRole.PolicyProperty.PolicyName` 16 | * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-policy.html#cfn-iam-policies-policyname 17 | */ 18 | readonly policyName: string; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/identityprovider.ts: -------------------------------------------------------------------------------- 1 | import { Construct, Duration, Resource, Token } from '@aws-cdk/core'; 2 | import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation'; 3 | import { PolicyStatement } from '@aws-cdk/aws-iam'; 4 | import { Function, Code, Runtime } from '@aws-cdk/aws-lambda'; 5 | import * as path from 'path'; 6 | 7 | export interface OIDCIdentityProviderProps { 8 | /** 9 | * The EKS cluster name. 10 | */ 11 | readonly clusterName: string; 12 | } 13 | 14 | export class OIDCIdentityProvider extends Resource { 15 | /** 16 | * Returns the ARN of the identity provider. 17 | * 18 | * @attribute 19 | */ 20 | public readonly providerArn: string; 21 | 22 | private static fn: Function; 23 | constructor(scope: Construct, id: string, props: OIDCIdentityProviderProps) { 24 | super(scope, id); 25 | 26 | if (!OIDCIdentityProvider.fn) { 27 | OIDCIdentityProvider.fn = new Function(scope, 'OIDCIdentityProviderCustomResource', { 28 | code: Code.fromAsset(path.resolve(__dirname, '..', 'lambda-packages', 'identity_provider_handler')), 29 | handler: 'index.handler', 30 | runtime: Runtime.NODEJS_12_X, 31 | timeout: Duration.minutes(15), 32 | }); 33 | OIDCIdentityProvider.fn.addToRolePolicy(new PolicyStatement({ 34 | actions: [ 35 | 'eks:DescribeCluster', 36 | 'iam:CreateOpenIDConnectProvider', 37 | 'iam:DeleteOpenIDConnectProvider' 38 | ], 39 | resources: ['*'] 40 | })); 41 | } 42 | 43 | const provider = new CustomResource(this, 'Resource', { 44 | provider: CustomResourceProvider.fromLambda(OIDCIdentityProvider.fn), 45 | resourceType: 'Custom::EksOidcIdentityProvider', 46 | properties: { 47 | ClusterName: props.clusterName, 48 | } 49 | }); 50 | this.providerArn = Token.asString(provider.getAtt('Arn')); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './role'; 2 | export * from './identityprovider'; 3 | -------------------------------------------------------------------------------- /lib/role.ts: -------------------------------------------------------------------------------- 1 | import { Construct, Duration, Lazy, Resource, Token } from '@aws-cdk/core'; 2 | import { Grant, IManagedPolicy, Policy, PolicyDocument, PolicyStatement, ArnPrincipal, IPrincipal, PrincipalPolicyFragment, IRole, AddToPrincipalPolicyResult } from '@aws-cdk/aws-iam'; 3 | import { AttachedPolicies } from './util'; 4 | import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation'; 5 | import { Function, Code, Runtime } from '@aws-cdk/aws-lambda'; 6 | import { CfnRole } from './cfn'; 7 | import * as path from 'path'; 8 | 9 | 10 | export interface RoleProps { 11 | /** 12 | * The EKS cluster name. 13 | */ 14 | readonly clusterName: string; 15 | 16 | /** 17 | * The Kubernetes namespace in which the service account lives. 18 | * @default default 19 | */ 20 | readonly namespace?: string; 21 | 22 | /** 23 | * The Kubernetes service account that will be allowed to assume the IAM Role. 24 | */ 25 | readonly serviceAccount: string; 26 | 27 | /** 28 | * A list of managed policies associated with this role. 29 | * 30 | * You can add managed policies later using 31 | * `addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(policyName))`. 32 | * 33 | * @default - No managed policies. 34 | */ 35 | readonly managedPolicies?: IManagedPolicy[]; 36 | /** 37 | * A list of named policies to inline into this role. These policies will be 38 | * created with the role, whereas those added by ``addToPolicy`` are added 39 | * using a separate CloudFormation resource (allowing a way around circular 40 | * dependencies that could otherwise be introduced). 41 | * 42 | * @default - No policy is inlined in the Role resource. 43 | */ 44 | readonly inlinePolicies?: { 45 | [name: string]: PolicyDocument; 46 | }; 47 | /** 48 | * The path associated with this role. For information about IAM paths, see 49 | * Friendly Names and Paths in IAM User Guide. 50 | * 51 | * @default / 52 | */ 53 | readonly path?: string; 54 | /** 55 | * AWS supports permissions boundaries for IAM entities (users or roles). 56 | * A permissions boundary is an advanced feature for using a managed policy 57 | * to set the maximum permissions that an identity-based policy can grant to 58 | * an IAM entity. An entity's permissions boundary allows it to perform only 59 | * the actions that are allowed by both its identity-based policies and its 60 | * permissions boundaries. 61 | * 62 | * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-permissionsboundary 63 | * @link https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html 64 | * 65 | * @default - No permissions boundary. 66 | */ 67 | readonly permissionsBoundary?: IManagedPolicy; 68 | /** 69 | * A name for the IAM role. For valid values, see the RoleName parameter for 70 | * the CreateRole action in the IAM API Reference. 71 | * 72 | * IMPORTANT: If you specify a name, you cannot perform updates that require 73 | * replacement of this resource. You can perform updates that require no or 74 | * some interruption. If you must replace the resource, specify a new name. 75 | * 76 | * If you specify a name, you must specify the CAPABILITY_NAMED_IAM value to 77 | * acknowledge your template's capabilities. For more information, see 78 | * Acknowledging IAM Resources in AWS CloudFormation Templates. 79 | * 80 | * @default - AWS CloudFormation generates a unique physical ID and uses that ID 81 | * for the group name. 82 | */ 83 | readonly roleName?: string; 84 | /** 85 | * The maximum session duration that you want to set for the specified role. 86 | * This setting can have a value from 1 hour (3600sec) to 12 (43200sec) hours. 87 | * 88 | * Anyone who assumes the role from the AWS CLI or API can use the 89 | * DurationSeconds API parameter or the duration-seconds CLI parameter to 90 | * request a longer session. The MaxSessionDuration setting determines the 91 | * maximum duration that can be requested using the DurationSeconds 92 | * parameter. 93 | * 94 | * If users don't specify a value for the DurationSeconds parameter, their 95 | * security credentials are valid for one hour by default. This applies when 96 | * you use the AssumeRole* API operations or the assume-role* CLI operations 97 | * but does not apply when you use those operations to create a console URL. 98 | * 99 | * @link https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html 100 | * 101 | * @default Duration.hours(1) 102 | */ 103 | readonly maxSessionDuration?: Duration; 104 | /** 105 | * A description of the role. It can be up to 1000 characters long. 106 | * 107 | * @default - No description. 108 | */ 109 | readonly description?: string; 110 | } 111 | 112 | /** 113 | * IAM Role 114 | * 115 | * Defines an IAM role. The role is created with an assume policy document associated with 116 | * the specified AWS service principal defined in `serviceAssumeRole`. 117 | */ 118 | export class Role extends Resource implements IRole { 119 | 120 | public readonly grantPrincipal: IPrincipal = this; 121 | 122 | public readonly assumeRoleAction: string = 'sts:AssumeRoleWithWebIdentity'; 123 | 124 | /** 125 | * Returns the ARN of this role. 126 | */ 127 | public readonly roleArn: string; 128 | 129 | /** 130 | * Returns the stable and unique string identifying the role. For example, 131 | * AIDAJQABLZS4A3QDU576Q. 132 | * 133 | * @attribute 134 | */ 135 | public readonly roleId: string; 136 | 137 | /** 138 | * Returns the name of the role. 139 | */ 140 | public readonly roleName: string; 141 | 142 | /** 143 | * Returns the role. 144 | */ 145 | public readonly policyFragment: PrincipalPolicyFragment; 146 | 147 | /** 148 | * Returns the permissions boundary attached to this role 149 | */ 150 | public readonly permissionsBoundary?: IManagedPolicy; 151 | 152 | private defaultPolicy?: Policy; 153 | private readonly managedPolicies: IManagedPolicy[] = []; 154 | private readonly attachedPolicies = new AttachedPolicies(); 155 | 156 | private static fn: Function; 157 | 158 | constructor(scope: Construct, id: string, props: RoleProps) { 159 | super(scope, id, { 160 | physicalName: props.roleName, 161 | }); 162 | 163 | this.managedPolicies.push(...props.managedPolicies || []); 164 | this.permissionsBoundary = props.permissionsBoundary; 165 | const maxSessionDuration = props.maxSessionDuration && props.maxSessionDuration.toSeconds(); 166 | validateMaxSessionDuration(maxSessionDuration); 167 | const description = (props.description && props.description?.length > 0) ? props.description : undefined; 168 | 169 | if (description && description.length > 1000) { 170 | throw new Error('Role description must be no longer than 1000 characters.'); 171 | } 172 | 173 | if (!Role.fn) { 174 | Role.fn = new Function(scope, 'IAMRoleForK8SSvcAcctCustomResource', { 175 | code: Code.fromAsset(path.resolve(__dirname, '..', 'lambda-packages', 'role_handler')), 176 | handler: 'index.handler', 177 | runtime: Runtime.NODEJS_12_X, 178 | timeout: Duration.minutes(15), 179 | }); 180 | Role.fn.addToRolePolicy(new PolicyStatement({ 181 | actions: [ 182 | 'eks:DescribeCluster', 183 | 'iam:AttachRolePolicy', 184 | 'iam:CreateRole', 185 | 'iam:DeleteRole', 186 | 'iam:DeleteRolePolicy', 187 | 'iam:DescribeRole', 188 | 'iam:DetachRolePolicy', 189 | 'iam:GetRole', 190 | 'iam:ListAttachedRolePolicies', 191 | 'iam:ListRoles', 192 | 'iam:PutRolePermissionsBoundary', 193 | 'iam:PutRolePolicy', 194 | 'iam:TagRole', 195 | 'iam:UntagRole', 196 | 'iam:UpdateAssumeRolePolicy', 197 | 'iam:UpdateRole', 198 | 'sts:GetCallerIdentity'], 199 | resources: ['*'] 200 | })); 201 | } 202 | 203 | const role = new CustomResource(this, 'Resource', { 204 | provider: CustomResourceProvider.fromLambda(Role.fn), 205 | resourceType: 'Custom::IamRoleForServiceAccount', 206 | properties: { 207 | ClusterName: props.clusterName, 208 | Namespace: props.namespace || 'default', 209 | ServiceAccount: props.serviceAccount, 210 | ManagedPolicyArns: Lazy.listValue({ produce: () => this.managedPolicies.map(p => p.managedPolicyArn) }, { omitEmpty: true }), 211 | Policies: _flatten(props.inlinePolicies), 212 | Path: props.path, 213 | PermissionsBoundary: this.permissionsBoundary ? this.permissionsBoundary.managedPolicyArn : undefined, 214 | RoleName: this.physicalName, 215 | MaxSessionDuration: maxSessionDuration, 216 | Description: description, 217 | } 218 | }) 219 | 220 | this.roleId = Token.asString(role.getAtt('RoleId')); 221 | this.roleArn = this.getResourceArnAttribute(Token.asString(role.getAtt('Arn')), { 222 | region: '', // IAM is global in each partition 223 | service: 'iam', 224 | resource: 'role', 225 | resourceName: this.physicalName, 226 | }); 227 | this.roleName = this.getResourceNameAttribute(role.ref); 228 | this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment; 229 | 230 | function _flatten(policies?: { [name: string]: PolicyDocument }) { 231 | if (policies == null || Object.keys(policies).length === 0) { 232 | return undefined; 233 | } 234 | const result = new Array(); 235 | for (const policyName of Object.keys(policies)) { 236 | const policyDocument = policies[policyName]; 237 | result.push({ policyName, policyDocument }); 238 | } 239 | return result; 240 | } 241 | } 242 | 243 | /** 244 | * Adds a permission to the role's default policy document. 245 | * If there is no default policy attached to this role, it will be created. 246 | * @param statement The permission statement to add to the policy document 247 | */ 248 | public addToPolicy(statement: PolicyStatement): boolean { 249 | if (!this.defaultPolicy) { 250 | this.defaultPolicy = new Policy(this, 'DefaultPolicy'); 251 | this.attachInlinePolicy(this.defaultPolicy); 252 | } 253 | this.defaultPolicy.addStatements(statement); 254 | return true; 255 | } 256 | 257 | /** 258 | * Attaches a managed policy to this role. 259 | * @param policy The managed policy to attach. 260 | */ 261 | public addManagedPolicy(policy: IManagedPolicy) { 262 | if (this.managedPolicies.find(mp => mp === policy)) { return; } 263 | this.managedPolicies.push(policy); 264 | } 265 | 266 | /** 267 | * Adds a permission to the role's default policy document. 268 | * If there is no default policy attached to this role, it will be created. 269 | * @param statement The permission statement to add to the policy document 270 | */ 271 | public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { 272 | if (!this.defaultPolicy) { 273 | this.defaultPolicy = new Policy(this, 'DefaultPolicy'); 274 | this.attachInlinePolicy(this.defaultPolicy); 275 | } 276 | this.defaultPolicy.addStatements(statement); 277 | return { statementAdded: true, policyDependable: this.defaultPolicy }; 278 | } 279 | 280 | /** 281 | * Attaches a policy to this role. 282 | * @param policy The policy to attach 283 | */ 284 | public attachInlinePolicy(policy: Policy) { 285 | this.attachedPolicies.attach(policy); 286 | policy.attachToRole(this); 287 | } 288 | 289 | /** 290 | * Grant the actions defined in actions to the identity Principal on this resource. 291 | */ 292 | public grant(grantee: IPrincipal, ...actions: string[]) { 293 | return Grant.addToPrincipal({ 294 | grantee, 295 | actions, 296 | resourceArns: [this.roleArn], 297 | scope: this 298 | }); 299 | } 300 | 301 | /** 302 | * Grant permissions to the given principal to pass this role. 303 | */ 304 | public grantPassRole(identity: IPrincipal) { 305 | return this.grant(identity, 'iam:PassRole'); 306 | } 307 | 308 | /** 309 | * Grant permissions to the given principal to assume this role. 310 | */ 311 | public grantAssumeRole(grantee: IPrincipal) { 312 | return this.grant(grantee, 'sts:AssumeRole'); 313 | } 314 | } 315 | 316 | 317 | function validateMaxSessionDuration(duration?: number) { 318 | if (duration === undefined) { 319 | return; 320 | } 321 | 322 | if (duration < 3600 || duration > 43200) { 323 | throw new Error(`maxSessionDuration is set to ${duration}, but must be >= 3600sec (1hr) and <= 43200sec (12hrs)`); 324 | } 325 | } 326 | 327 | /** 328 | * A PolicyStatement that normalizes its Principal field differently 329 | * 330 | * Normally, "anyone" is normalized to "Principal: *", but this statement 331 | * normalizes to "Principal: { AWS: * }". 332 | */ 333 | class AwsStarStatement extends PolicyStatement { 334 | public toStatementJson(): any { 335 | const stat = super.toStatementJson(); 336 | 337 | if (stat.Principal === '*') { 338 | stat.Principal = { AWS: '*' }; 339 | } 340 | 341 | return stat; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | import { DefaultTokenResolver, IConstruct, Lazy, StringConcat, Tokenization } from '@aws-cdk/core'; 2 | import { IPolicy } from '@aws-cdk/aws-iam'; 3 | 4 | const MAX_POLICY_NAME_LEN = 128; 5 | 6 | export function undefinedIfEmpty(f: () => string[]): string[] { 7 | return Lazy.listValue({ produce: () => { 8 | const array = f(); 9 | return (array && array.length > 0) ? array : undefined; 10 | }}); 11 | } 12 | 13 | /** 14 | * Used to generate a unique policy name based on the policy resource construct. 15 | * The logical ID of the resource is a great candidate as long as it doesn't exceed 16 | * 128 characters, so we take the last 128 characters (in order to make sure the hash 17 | * is there). 18 | */ 19 | export function generatePolicyName(scope: IConstruct, logicalId: string): string { 20 | // as logicalId is itself a Token, resolve it first 21 | const resolvedLogicalId = Tokenization.resolve(logicalId, { 22 | scope, 23 | resolver: new DefaultTokenResolver(new StringConcat()), 24 | }); 25 | return lastNCharacters(resolvedLogicalId, MAX_POLICY_NAME_LEN); 26 | } 27 | 28 | /** 29 | * Returns a string composed of the last n characters of str. 30 | * If str is shorter than n, returns str. 31 | * 32 | * @param str the string to return the last n characters of 33 | * @param n how many characters to return 34 | */ 35 | function lastNCharacters(str: string, n: number) { 36 | const startIndex = Math.max(str.length - n, 0); 37 | return str.substring(startIndex, str.length); 38 | } 39 | 40 | /** 41 | * Helper class that maintains the set of attached policies for a principal. 42 | */ 43 | export class AttachedPolicies { 44 | private policies = new Array(); 45 | 46 | /** 47 | * Adds a policy to the list of attached policies. 48 | * 49 | * If this policy is already, attached, returns false. 50 | * If there is another policy attached with the same name, throws an exception. 51 | */ 52 | public attach(policy: IPolicy) { 53 | if (this.policies.find(p => p === policy)) { 54 | return; // already attached 55 | } 56 | 57 | if (this.policies.find(p => p.policyName === policy.policyName)) { 58 | throw new Error(`A policy named "${policy.policyName}" is already attached`); 59 | } 60 | 61 | this.policies.push(policy); 62 | } 63 | } 64 | 65 | /** 66 | * Merge two dictionaries that represent IAM principals 67 | */ 68 | export function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) { 69 | for (const key of Object.keys(source)) { 70 | target[key] = target[key] || []; 71 | 72 | let value = source[key]; 73 | if (!Array.isArray(value)) { 74 | value = [ value ]; 75 | } 76 | 77 | target[key].push(...value); 78 | } 79 | 80 | return target; 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-eks-irsa-cfn", 3 | "version": "0.1.2", 4 | "author": "Michael S. Fischer", 5 | "license": "Apache-2.0", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "scripts": { 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest && jest -c lambda-packages/identity_provider_handler/jest.config.js && jest -c lambda-packages/role_handler/jest.config.js", 12 | "prepare": "tsc" 13 | }, 14 | "devDependencies": { 15 | "@aws-cdk/assert": "^1.101.0", 16 | "@types/jest": "^26.0.23", 17 | "@types/node": "^15.0.0", 18 | "aws-cdk": "^1.101.0", 19 | "jest": "^26.6.0", 20 | "ts-jest": "^26.1.4", 21 | "typescript": "^4.2.4" 22 | }, 23 | "dependencies": { 24 | "@aws-cdk/aws-cloudformation": "^1.101.0", 25 | "@aws-cdk/aws-iam": "^1.101.0", 26 | "@aws-cdk/aws-lambda": "^1.101.0", 27 | "@aws-cdk/core": "^1.101.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/irsa.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import { expect as expectCDK, haveResource, SynthUtils } from '@aws-cdk/assert'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import CdkEksIamRoleServiceaccount = require('../lib/index'); 5 | 6 | test('SQS Queue Created', () => { 7 | const app = new cdk.App(); 8 | const stack = new cdk.Stack(app, "TestStack"); 9 | // WHEN 10 | new CdkEksIamRoleServiceaccount.Role(stack, 'MyTestConstruct'); 11 | // THEN 12 | expectCDK(stack).to(haveResource("AWS::SQS::Queue")); 13 | }); 14 | 15 | test('SNS Topic Created', () => { 16 | const app = new cdk.App(); 17 | const stack = new cdk.Stack(app, "TestStack"); 18 | // WHEN 19 | new CdkEksIamRoleServiceaccount.Role(stack, 'MyTestConstruct'); 20 | // THEN 21 | expectCDK(stack).to(haveResource("AWS::SNS::Topic")); 22 | }); 23 | */ 24 | 25 | test.todo('Role Created'); 26 | test.todo('OIDC Identity Provider Created'); 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | 25 | --------------------------------------------------------------------------------