├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── API.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── package.json ├── rfc └── README.md ├── src ├── account-handler │ └── index.ts ├── account-provider.ts ├── account.ts ├── aws-config-recorder.ts ├── aws-organizations-stack.ts ├── dns-stack.ts ├── dns.ts ├── dns │ ├── cross-account-dns-delegator.ts │ ├── cross-account-zone-delegation-record-provider.ts │ ├── cross-account-zone-delegation-record.ts │ └── delegation-record-handler │ │ ├── index.ts │ │ └── utils.ts ├── index.ts ├── organization-handler │ └── index.ts ├── organization-provider.ts ├── organization-trail.ts ├── organization.ts ├── organizational-unit-provider.ts ├── organizational-unit.ts ├── ou-handler │ └── index.ts ├── secure-root-user.ts ├── validate-email-handler │ └── index.ts ├── validate-email-provider.ts └── validate-email.ts ├── test ├── integ │ ├── organization-unit-provider.test.ts │ └── utils.ts └── unit │ ├── account-provider.test.ts │ ├── account.test.ts │ ├── aws-organizations-stack.test.ts │ ├── cross-account-dns-delegator-provider.test.ts │ ├── cross-account-dns-delegator.test.ts │ ├── organization-provider.test.ts │ ├── organizational-trail.test.ts │ ├── organizational-unit-provider.test.ts │ ├── secure-root-user.test.ts │ └── validate-email.test.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "!.projenrc.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/build.yml linguist-generated 8 | /.github/workflows/pull-request-lint.yml linguist-generated 9 | /.github/workflows/release.yml linguist-generated 10 | /.github/workflows/upgrade-main.yml linguist-generated 11 | /.gitignore linguist-generated 12 | /.mergify.yml linguist-generated 13 | /.npmignore linguist-generated 14 | /.projen/** linguist-generated 15 | /.projen/deps.json linguist-generated 16 | /.projen/files.json linguist-generated 17 | /.projen/tasks.json linguist-generated 18 | /API.md linguist-generated 19 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > .repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | - name: Upload patch 36 | if: steps.self_mutation.outputs.self_mutation_happened 37 | uses: actions/upload-artifact@v3 38 | with: 39 | name: .repo.patch 40 | path: .repo.patch 41 | - name: Fail build on mutation 42 | if: steps.self_mutation.outputs.self_mutation_happened 43 | run: |- 44 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 45 | cat .repo.patch 46 | exit 1 47 | - name: Backup artifact permissions 48 | run: cd dist && getfacl -R . > permissions-backup.acl 49 | continue-on-error: true 50 | - name: Upload artifact 51 | uses: actions/upload-artifact@v3 52 | with: 53 | name: build-artifact 54 | path: dist 55 | self-mutation: 56 | needs: build 57 | runs-on: ubuntu-latest 58 | permissions: 59 | contents: write 60 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 61 | steps: 62 | - name: Checkout 63 | uses: actions/checkout@v3 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | ref: ${{ github.event.pull_request.head.ref }} 67 | repository: ${{ github.event.pull_request.head.repo.full_name }} 68 | - name: Download patch 69 | uses: actions/download-artifact@v3 70 | with: 71 | name: .repo.patch 72 | path: ${{ runner.temp }} 73 | - name: Apply patch 74 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 75 | - name: Set git identity 76 | run: |- 77 | git config user.name "github-actions" 78 | git config user.email "github-actions@github.com" 79 | - name: Push changes 80 | run: |2- 81 | git add . 82 | git commit -s -m "chore: self mutation" 83 | git push origin HEAD:${{ github.event.pull_request.head.ref }} 84 | package-js: 85 | needs: build 86 | runs-on: ubuntu-latest 87 | permissions: {} 88 | if: "! needs.build.outputs.self_mutation_happened" 89 | steps: 90 | - uses: actions/setup-node@v3 91 | with: 92 | node-version: 16 93 | - name: Download build artifacts 94 | uses: actions/download-artifact@v3 95 | with: 96 | name: build-artifact 97 | path: dist 98 | - name: Restore build artifact permissions 99 | run: cd dist && setfacl --restore=permissions-backup.acl 100 | continue-on-error: true 101 | - name: Prepare Repository 102 | run: mv dist .repo 103 | - name: Install Dependencies 104 | run: cd .repo && yarn install --check-files --frozen-lockfile 105 | - name: Create js artifact 106 | run: cd .repo && npx projen package:js 107 | - name: Collect js Artifact 108 | run: mv .repo/dist dist 109 | package-python: 110 | needs: build 111 | runs-on: ubuntu-latest 112 | permissions: {} 113 | if: "! needs.build.outputs.self_mutation_happened" 114 | steps: 115 | - uses: actions/setup-node@v3 116 | with: 117 | node-version: 16 118 | - uses: actions/setup-python@v4 119 | with: 120 | python-version: 3.x 121 | - name: Download build artifacts 122 | uses: actions/download-artifact@v3 123 | with: 124 | name: build-artifact 125 | path: dist 126 | - name: Restore build artifact permissions 127 | run: cd dist && setfacl --restore=permissions-backup.acl 128 | continue-on-error: true 129 | - name: Prepare Repository 130 | run: mv dist .repo 131 | - name: Install Dependencies 132 | run: cd .repo && yarn install --check-files --frozen-lockfile 133 | - name: Create python artifact 134 | run: cd .repo && npx projen package:python 135 | - name: Collect python Artifact 136 | run: mv .repo/dist dist 137 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.0.2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 16 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Check for new commits 36 | id: git_remote 37 | run: echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 38 | - name: Backup artifact permissions 39 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 40 | run: cd dist && getfacl -R . > permissions-backup.acl 41 | continue-on-error: true 42 | - name: Upload artifact 43 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 44 | uses: actions/upload-artifact@v3 45 | with: 46 | name: build-artifact 47 | path: dist 48 | release_github: 49 | name: Publish to GitHub Releases 50 | needs: release 51 | runs-on: ubuntu-latest 52 | permissions: 53 | contents: write 54 | if: needs.release.outputs.latest_commit == github.sha 55 | steps: 56 | - uses: actions/setup-node@v3 57 | with: 58 | node-version: 16 59 | - name: Download build artifacts 60 | uses: actions/download-artifact@v3 61 | with: 62 | name: build-artifact 63 | path: dist 64 | - name: Restore build artifact permissions 65 | run: cd dist && setfacl --restore=permissions-backup.acl 66 | continue-on-error: true 67 | - name: Prepare Repository 68 | run: mv dist .repo 69 | - name: Collect GitHub Metadata 70 | run: mv .repo/dist dist 71 | - name: Release 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | GITHUB_REPOSITORY: ${{ github.repository }} 75 | GITHUB_REF: ${{ github.ref }} 76 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 77 | release_npm: 78 | name: Publish to npm 79 | needs: release 80 | runs-on: ubuntu-latest 81 | permissions: 82 | contents: read 83 | if: needs.release.outputs.latest_commit == github.sha 84 | steps: 85 | - uses: actions/setup-node@v3 86 | with: 87 | node-version: 16 88 | - name: Download build artifacts 89 | uses: actions/download-artifact@v3 90 | with: 91 | name: build-artifact 92 | path: dist 93 | - name: Restore build artifact permissions 94 | run: cd dist && setfacl --restore=permissions-backup.acl 95 | continue-on-error: true 96 | - name: Prepare Repository 97 | run: mv dist .repo 98 | - name: Install Dependencies 99 | run: cd .repo && yarn install --check-files --frozen-lockfile 100 | - name: Create js artifact 101 | run: cd .repo && npx projen package:js 102 | - name: Collect js Artifact 103 | run: mv .repo/dist dist 104 | - name: Release 105 | env: 106 | NPM_DIST_TAG: latest 107 | NPM_REGISTRY: registry.npmjs.org 108 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 109 | run: npx -p publib@latest publib-npm 110 | release_pypi: 111 | name: Publish to PyPI 112 | needs: release 113 | runs-on: ubuntu-latest 114 | permissions: 115 | contents: read 116 | if: needs.release.outputs.latest_commit == github.sha 117 | steps: 118 | - uses: actions/setup-node@v3 119 | with: 120 | node-version: 16 121 | - uses: actions/setup-python@v4 122 | with: 123 | python-version: 3.x 124 | - name: Download build artifacts 125 | uses: actions/download-artifact@v3 126 | with: 127 | name: build-artifact 128 | path: dist 129 | - name: Restore build artifact permissions 130 | run: cd dist && setfacl --restore=permissions-backup.acl 131 | continue-on-error: true 132 | - name: Prepare Repository 133 | run: mv dist .repo 134 | - name: Install Dependencies 135 | run: cd .repo && yarn install --check-files --frozen-lockfile 136 | - name: Create python artifact 137 | run: cd .repo && npx projen package:python 138 | - name: Collect python Artifact 139 | run: mv .repo/dist dist 140 | - name: Release 141 | env: 142 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 143 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 144 | run: npx -p publib@latest publib-pypi 145 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > .repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | - name: Upload patch 35 | if: steps.create_patch.outputs.patch_created 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: .repo.patch 39 | path: .repo.patch 40 | pr: 41 | name: Create Pull Request 42 | needs: upgrade 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: read 46 | if: ${{ needs.upgrade.outputs.patch_created }} 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v3 50 | with: 51 | ref: main 52 | - name: Download patch 53 | uses: actions/download-artifact@v3 54 | with: 55 | name: .repo.patch 56 | path: ${{ runner.temp }} 57 | - name: Apply patch 58 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 59 | - name: Set git identity 60 | run: |- 61 | git config user.name "github-actions" 62 | git config user.email "github-actions@github.com" 63 | - name: Create Pull Request 64 | id: create-pr 65 | uses: peter-evans/create-pull-request@v4 66 | with: 67 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 68 | commit-message: |- 69 | chore(deps): upgrade dependencies 70 | 71 | Upgrades project dependencies. See details in [workflow run]. 72 | 73 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 74 | 75 | ------ 76 | 77 | *Automatically created by projen via the "upgrade-main" workflow* 78 | branch: github-actions/upgrade-main 79 | title: "chore(deps): upgrade dependencies" 80 | body: |- 81 | Upgrades project dependencies. See details in [workflow run]. 82 | 83 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 84 | 85 | ------ 86 | 87 | *Automatically created by projen via the "upgrade-main" workflow* 88 | author: github-actions 89 | committer: github-actions 90 | signoff: true 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | !/.projenrc.js 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.mergify.yml 42 | !/.github/workflows/upgrade-main.yml 43 | !/.github/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.dev.json 46 | !/src/ 47 | /lib 48 | /dist/ 49 | !/.eslintrc.json 50 | .jsii 51 | tsconfig.json 52 | !/API.md 53 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | conditions: 6 | - "#approved-reviews-by>=1" 7 | - -label~=(do-not-merge) 8 | - status-success=build 9 | - status-success=package-js 10 | - status-success=package-python 11 | pull_request_rules: 12 | - name: Automatic merge on approval and successful build 13 | actions: 14 | delete_head_branch: {} 15 | queue: 16 | method: squash 17 | name: default 18 | commit_message_template: |- 19 | {{ title }} (#{{ number }}) 20 | 21 | {{ body }} 22 | conditions: 23 | - "#approved-reviews-by>=1" 24 | - -label~=(do-not-merge) 25 | - status-success=build 26 | - status-success=package-js 27 | - status-success=package-python 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-cdk/assert", 5 | "version": "2.60.0", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@aws-cdk/cx-api", 10 | "version": "2.60.0", 11 | "type": "build" 12 | }, 13 | { 14 | "name": "@types/aws-lambda", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@types/jest", 19 | "version": "^27", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "@types/node", 24 | "version": "^16", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "@types/sinon", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "@typescript-eslint/eslint-plugin", 33 | "version": "^5", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "@typescript-eslint/parser", 38 | "version": "^5", 39 | "type": "build" 40 | }, 41 | { 42 | "name": "aws-cdk-lib", 43 | "version": "2.60.0", 44 | "type": "build" 45 | }, 46 | { 47 | "name": "aws-cdk", 48 | "version": "2.60.0", 49 | "type": "build" 50 | }, 51 | { 52 | "name": "aws-sdk-mock", 53 | "type": "build" 54 | }, 55 | { 56 | "name": "cdk-assets", 57 | "version": "2.60.0", 58 | "type": "build" 59 | }, 60 | { 61 | "name": "constructs", 62 | "version": "10.0.5", 63 | "type": "build" 64 | }, 65 | { 66 | "name": "eslint-import-resolver-node", 67 | "type": "build" 68 | }, 69 | { 70 | "name": "eslint-import-resolver-typescript", 71 | "type": "build" 72 | }, 73 | { 74 | "name": "eslint-plugin-import", 75 | "type": "build" 76 | }, 77 | { 78 | "name": "eslint", 79 | "version": "^8", 80 | "type": "build" 81 | }, 82 | { 83 | "name": "jest-junit", 84 | "version": "^13", 85 | "type": "build" 86 | }, 87 | { 88 | "name": "jest-runner-groups", 89 | "type": "build" 90 | }, 91 | { 92 | "name": "jest", 93 | "version": "^27", 94 | "type": "build" 95 | }, 96 | { 97 | "name": "jsii", 98 | "type": "build" 99 | }, 100 | { 101 | "name": "jsii-diff", 102 | "type": "build" 103 | }, 104 | { 105 | "name": "jsii-docgen", 106 | "type": "build" 107 | }, 108 | { 109 | "name": "jsii-pacmak", 110 | "type": "build" 111 | }, 112 | { 113 | "name": "json-schema", 114 | "type": "build" 115 | }, 116 | { 117 | "name": "npm-check-updates", 118 | "version": "^16", 119 | "type": "build" 120 | }, 121 | { 122 | "name": "projen", 123 | "type": "build" 124 | }, 125 | { 126 | "name": "promptly", 127 | "type": "build" 128 | }, 129 | { 130 | "name": "sinon", 131 | "type": "build" 132 | }, 133 | { 134 | "name": "standard-version", 135 | "version": "^9", 136 | "type": "build" 137 | }, 138 | { 139 | "name": "ts-jest", 140 | "version": "^27", 141 | "type": "build" 142 | }, 143 | { 144 | "name": "typescript", 145 | "type": "build" 146 | }, 147 | { 148 | "name": "@types/babel__traverse", 149 | "version": "7.18.2", 150 | "type": "override" 151 | }, 152 | { 153 | "name": "@types/prettier", 154 | "version": "2.6.0", 155 | "type": "override" 156 | }, 157 | { 158 | "name": "aws-cdk-lib", 159 | "version": "^2.60.0", 160 | "type": "peer" 161 | }, 162 | { 163 | "name": "constructs", 164 | "version": "^10.0.5", 165 | "type": "peer" 166 | } 167 | ], 168 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 169 | } 170 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/release.yml", 9 | ".github/workflows/upgrade-main.yml", 10 | ".gitignore", 11 | ".mergify.yml", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "LICENSE", 16 | "tsconfig.dev.json" 17 | ], 18 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 19 | } 20 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "" 36 | }, 37 | "steps": [ 38 | { 39 | "builtin": "release/bump-version" 40 | } 41 | ], 42 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\"" 43 | }, 44 | "clobber": { 45 | "name": "clobber", 46 | "description": "hard resets to HEAD of origin and cleans the local repo", 47 | "env": { 48 | "BRANCH": "$(git branch --show-current)" 49 | }, 50 | "steps": [ 51 | { 52 | "exec": "git checkout -b scratch", 53 | "name": "save current HEAD in \"scratch\" branch" 54 | }, 55 | { 56 | "exec": "git checkout $BRANCH" 57 | }, 58 | { 59 | "exec": "git fetch origin", 60 | "name": "fetch latest changes from origin" 61 | }, 62 | { 63 | "exec": "git reset --hard origin/$BRANCH", 64 | "name": "hard reset to origin commit" 65 | }, 66 | { 67 | "exec": "git clean -fdx", 68 | "name": "clean all untracked files" 69 | }, 70 | { 71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 72 | } 73 | ], 74 | "condition": "git diff --exit-code > /dev/null" 75 | }, 76 | "compat": { 77 | "name": "compat", 78 | "description": "Perform API compatibility check against latest version", 79 | "steps": [ 80 | { 81 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 82 | } 83 | ] 84 | }, 85 | "compile": { 86 | "name": "compile", 87 | "description": "Only compile", 88 | "steps": [ 89 | { 90 | "exec": "jsii --silence-warnings=reserved-word" 91 | } 92 | ] 93 | }, 94 | "default": { 95 | "name": "default", 96 | "description": "Synthesize project files", 97 | "steps": [ 98 | { 99 | "exec": "node .projenrc.js" 100 | } 101 | ] 102 | }, 103 | "docgen": { 104 | "name": "docgen", 105 | "description": "Generate API.md from .jsii manifest", 106 | "steps": [ 107 | { 108 | "exec": "jsii-docgen -o API.md" 109 | } 110 | ] 111 | }, 112 | "eject": { 113 | "name": "eject", 114 | "description": "Remove projen from the project", 115 | "env": { 116 | "PROJEN_EJECTING": "true" 117 | }, 118 | "steps": [ 119 | { 120 | "spawn": "default" 121 | } 122 | ] 123 | }, 124 | "eslint": { 125 | "name": "eslint", 126 | "description": "Runs eslint against the codebase", 127 | "steps": [ 128 | { 129 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 130 | } 131 | ] 132 | }, 133 | "package": { 134 | "name": "package", 135 | "description": "Creates the distribution package", 136 | "steps": [ 137 | { 138 | "exec": "if [ ! -z ${CI} ]; then rsync -a . .repo --exclude .git --exclude node_modules && rm -rf dist && mv .repo dist; else npx projen package-all; fi" 139 | } 140 | ] 141 | }, 142 | "package-all": { 143 | "name": "package-all", 144 | "description": "Packages artifacts for all target languages", 145 | "steps": [ 146 | { 147 | "spawn": "package:js" 148 | }, 149 | { 150 | "spawn": "package:python" 151 | } 152 | ] 153 | }, 154 | "package:js": { 155 | "name": "package:js", 156 | "description": "Create js language bindings", 157 | "steps": [ 158 | { 159 | "exec": "jsii-pacmak -v --target js" 160 | } 161 | ] 162 | }, 163 | "package:python": { 164 | "name": "package:python", 165 | "description": "Create python language bindings", 166 | "steps": [ 167 | { 168 | "exec": "jsii-pacmak -v --target python" 169 | } 170 | ] 171 | }, 172 | "post-compile": { 173 | "name": "post-compile", 174 | "description": "Runs after successful compilation", 175 | "steps": [ 176 | { 177 | "spawn": "docgen" 178 | } 179 | ] 180 | }, 181 | "post-upgrade": { 182 | "name": "post-upgrade", 183 | "description": "Runs after upgrading dependencies" 184 | }, 185 | "pre-compile": { 186 | "name": "pre-compile", 187 | "description": "Prepare the project for compilation" 188 | }, 189 | "release": { 190 | "name": "release", 191 | "description": "Prepare a release from \"main\" branch", 192 | "env": { 193 | "RELEASE": "true" 194 | }, 195 | "steps": [ 196 | { 197 | "exec": "rm -fr dist" 198 | }, 199 | { 200 | "spawn": "bump" 201 | }, 202 | { 203 | "spawn": "build" 204 | }, 205 | { 206 | "spawn": "unbump" 207 | }, 208 | { 209 | "exec": "git diff --ignore-space-at-eol --exit-code" 210 | } 211 | ] 212 | }, 213 | "test": { 214 | "name": "test", 215 | "description": "Run tests", 216 | "steps": [ 217 | { 218 | "exec": "jest --group=unit" 219 | } 220 | ] 221 | }, 222 | "test:integ": { 223 | "name": "test:integ", 224 | "steps": [ 225 | { 226 | "exec": "jest --group=integ" 227 | } 228 | ] 229 | }, 230 | "test:unit": { 231 | "name": "test:unit", 232 | "steps": [ 233 | { 234 | "exec": "jest --group=unit" 235 | } 236 | ] 237 | }, 238 | "test:watch": { 239 | "name": "test:watch", 240 | "description": "Run jest in watch mode", 241 | "steps": [ 242 | { 243 | "exec": "jest --watch" 244 | } 245 | ] 246 | }, 247 | "unbump": { 248 | "name": "unbump", 249 | "description": "Restores version to 0.0.0", 250 | "env": { 251 | "OUTFILE": "package.json", 252 | "CHANGELOG": "dist/changelog.md", 253 | "BUMPFILE": "dist/version.txt", 254 | "RELEASETAG": "dist/releasetag.txt", 255 | "RELEASE_TAG_PREFIX": "" 256 | }, 257 | "steps": [ 258 | { 259 | "builtin": "release/reset-version" 260 | } 261 | ] 262 | }, 263 | "upgrade": { 264 | "name": "upgrade", 265 | "description": "upgrade dependencies", 266 | "env": { 267 | "CI": "0" 268 | }, 269 | "steps": [ 270 | { 271 | "exec": "yarn upgrade npm-check-updates" 272 | }, 273 | { 274 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='@aws-cdk/assert,@aws-cdk/cx-api,aws-cdk-lib,aws-cdk,cdk-assets,constructs'" 275 | }, 276 | { 277 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='@aws-cdk/assert,@aws-cdk/cx-api,aws-cdk-lib,aws-cdk,cdk-assets,constructs'" 278 | }, 279 | { 280 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='@aws-cdk/assert,@aws-cdk/cx-api,aws-cdk-lib,aws-cdk,cdk-assets,constructs'" 281 | }, 282 | { 283 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='@aws-cdk/assert,@aws-cdk/cx-api,aws-cdk-lib,aws-cdk,cdk-assets,constructs'" 284 | }, 285 | { 286 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='@aws-cdk/assert,@aws-cdk/cx-api,aws-cdk-lib,aws-cdk,cdk-assets,constructs'" 287 | }, 288 | { 289 | "exec": "yarn install --check-files" 290 | }, 291 | { 292 | "exec": "yarn upgrade" 293 | }, 294 | { 295 | "exec": "npx projen" 296 | }, 297 | { 298 | "spawn": "post-upgrade" 299 | } 300 | ] 301 | }, 302 | "watch": { 303 | "name": "watch", 304 | "description": "Watch & compile in the background", 305 | "steps": [ 306 | { 307 | "exec": "jsii -w --silence-warnings=reserved-word" 308 | } 309 | ] 310 | } 311 | }, 312 | "env": { 313 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 314 | }, 315 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 316 | } 317 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | const cdkVersion = '2.60.0'; 3 | const project = new awscdk.AwsCdkConstructLibrary({ 4 | author: 'AWS EMEA SA Specialist Builders', 5 | authorAddress: 'chazalf@amazon.com', 6 | cdkVersion: cdkVersion, 7 | defaultReleaseBranch: 'main', 8 | minNodeVersion: '16.0.0', 9 | workflowNodeVersion: 16, 10 | name: 'aws-bootstrap-kit', 11 | repositoryUrl: 'https://github.com/awslabs/aws-bootstrap-kit', 12 | publishToPypi: { 13 | distName: 'aws_ssabuilders.aws_bootstrap_kit', 14 | module: 'aws_ssabuilders.aws_bootstrap_kit', 15 | }, 16 | releaseToNpm: true, 17 | devDeps: [ 18 | `@aws-cdk/assert@${cdkVersion}`, 19 | `@aws-cdk/cx-api@${cdkVersion}`, 20 | `aws-cdk@${cdkVersion}`, 21 | `cdk-assets@${cdkVersion}`, 22 | '@types/aws-lambda', 23 | 'aws-sdk-mock', 24 | 'sinon', 25 | '@types/sinon', 26 | 'promptly', 27 | 'jest-runner-groups', 28 | ], 29 | jestOptions: { 30 | jestConfig: { 31 | runner: 'groups', 32 | }, 33 | }, 34 | 35 | }); 36 | 37 | project.testTask.reset('jest --group=unit'); 38 | 39 | project.addTask('test:unit', { 40 | exec: 'jest --group=unit', 41 | }); 42 | 43 | project.addTask('test:integ', { 44 | exec: 'jest --group=integ', 45 | }); 46 | 47 | project.synth(); 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.7.7](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.6...aws-bootstrap-kit@0.7.7) (2023-01-12) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * upgrade dependencies ([ae4a7eb](https://github.com/awslabs/aws-bootstrap-kit/commit/ae4a7ebed1cd3e3cb22afe7b1b55911f7187ed6a)) 12 | 13 | 14 | 15 | 16 | 17 | ## [0.7.6](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.5...aws-bootstrap-kit@0.7.6) (2022-10-28) 18 | 19 | **Note:** Version bump only for package aws-bootstrap-kit 20 | 21 | 22 | 23 | 24 | 25 | ## [0.7.5](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.3...aws-bootstrap-kit@0.7.5) (2022-10-12) 26 | 27 | 28 | 29 | ## 0.7.4 (2022-08-02) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * nestedOUs where raising error due to required accounts list in parent. Made accounts optional ([4fb745b](https://github.com/awslabs/aws-bootstrap-kit/commit/4fb745b0148cf6127e951723dae751672e979dcd)) 35 | 36 | 37 | 38 | 39 | 40 | # Changelog 41 | 42 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 43 | 44 | ### [0.7.4](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.15...v0.7.4) (2022-08-02) 45 | 46 | 47 | ### Features 48 | 49 | * allow peer dependencies using newer cdk versions ([ff93f19](https://github.com/awslabs/aws-bootstrap-kit/commit/ff93f193c369f78b3f58fed2be8ca17747023979)) 50 | * expose dns resources ([59c470c](https://github.com/awslabs/aws-bootstrap-kit/commit/59c470c6726969bb00b3ed4d621afec2b399407a)) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * nestedOUs where raising error due to required accounts list in parent. Made accounts optional ([4fb745b](https://github.com/awslabs/aws-bootstrap-kit/commit/4fb745b0148cf6127e951723dae751672e979dcd)) 56 | * python package with underscore ([a80f478](https://github.com/awslabs/aws-bootstrap-kit/commit/a80f478da4b3e74f73b04b302a8f49daedabb044)) 57 | * use account type STAGE instead of OU names ([decbba1](https://github.com/awslabs/aws-bootstrap-kit/commit/decbba137fdd907c58cf1603939bfc5394dc08e6)) 58 | 59 | ## [0.7.3](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.2...aws-bootstrap-kit@0.7.3) (2022-07-20) 60 | 61 | **Note:** Version bump only for package aws-bootstrap-kit 62 | 63 | 64 | 65 | 66 | 67 | ## [0.7.2](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.1...aws-bootstrap-kit@0.7.2) (2022-07-20) 68 | 69 | **Note:** Version bump only for package aws-bootstrap-kit 70 | 71 | 72 | 73 | 74 | 75 | ## [0.7.1](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.7.0...aws-bootstrap-kit@0.7.1) (2022-06-14) 76 | 77 | **Note:** Version bump only for package aws-bootstrap-kit 78 | 79 | 80 | 81 | 82 | 83 | # [0.7.0](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.6.1...aws-bootstrap-kit@0.7.0) (2022-03-03) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * use account type STAGE instead of OU names ([decbba1](https://github.com/awslabs/aws-bootstrap-kit/commit/decbba137fdd907c58cf1603939bfc5394dc08e6)) 89 | 90 | 91 | ### Features 92 | 93 | * expose dns resources ([59c470c](https://github.com/awslabs/aws-bootstrap-kit/commit/59c470c6726969bb00b3ed4d621afec2b399407a)) 94 | 95 | 96 | 97 | 98 | 99 | ## [0.6.1](https://github.com/awslabs/aws-bootstrap-kit/compare/aws-bootstrap-kit@0.6.0...aws-bootstrap-kit@0.6.1) (2022-02-24) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * python package with underscore ([a80f478](https://github.com/awslabs/aws-bootstrap-kit/commit/a80f478da4b3e74f73b04b302a8f49daedabb044)) 105 | 106 | 107 | 108 | 109 | 110 | # 0.6.0 (2022-02-15) 111 | 112 | 113 | ### Features 114 | 115 | * allow peer dependencies using newer cdk versions ([ff93f19](https://github.com/awslabs/aws-bootstrap-kit/commit/ff93f193c369f78b3f58fed2be8ca17747023979)) 116 | 117 | 118 | 119 | ## 0.3.15 (2021-10-08) 120 | 121 | 122 | ### Features 123 | 124 | * disable registerDelegatedAdministrator if not using DNS setup to unlock accounts creation ([0a7213e](https://github.com/awslabs/aws-bootstrap-kit/commit/0a7213e1380a03a225e898cc2fe4c9154ce6317c)) 125 | 126 | 127 | 128 | ## 0.3.9 (2021-04-01) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * CVE-2020-7774 ([8f2fd54](https://github.com/awslabs/aws-bootstrap-kit/commit/8f2fd54660c329872fbc3a6ca7f1349db006ae81)) 134 | 135 | 136 | 137 | ## 0.3.5 (2021-02-09) 138 | 139 | 140 | 141 | ## 0.3.4 (2021-02-09) 142 | 143 | 144 | 145 | ## 0.3.3 (2021-02-08) 146 | 147 | 148 | 149 | ## 0.3.2 (2021-02-03) 150 | 151 | 152 | 153 | ## 0.3.1 (2021-02-01) 154 | 155 | 156 | 157 | ## 0.2.9 (2021-01-18) 158 | 159 | 160 | 161 | ## 0.2.7 (2020-11-24) 162 | 163 | 164 | 165 | ## 0.2.6 (2020-11-09) 166 | 167 | 168 | 169 | 170 | 171 | # 0.5.0 (2022-02-15) 172 | 173 | 174 | ### Features 175 | 176 | * allow peer dependencies using newer cdk versions ([ff93f19](https://github.com/awslabs/aws-bootstrap-kit/commit/ff93f193c369f78b3f58fed2be8ca17747023979)) 177 | 178 | 179 | 180 | ## 0.3.15 (2021-10-08) 181 | 182 | 183 | ### Features 184 | 185 | * disable registerDelegatedAdministrator if not using DNS setup to unlock accounts creation ([0a7213e](https://github.com/awslabs/aws-bootstrap-kit/commit/0a7213e1380a03a225e898cc2fe4c9154ce6317c)) 186 | 187 | 188 | 189 | ## 0.3.9 (2021-04-01) 190 | 191 | 192 | ### Bug Fixes 193 | 194 | * CVE-2020-7774 ([8f2fd54](https://github.com/awslabs/aws-bootstrap-kit/commit/8f2fd54660c329872fbc3a6ca7f1349db006ae81)) 195 | 196 | 197 | 198 | ## 0.3.5 (2021-02-09) 199 | 200 | 201 | 202 | ## 0.3.4 (2021-02-09) 203 | 204 | 205 | 206 | ## 0.3.3 (2021-02-08) 207 | 208 | 209 | 210 | ## 0.3.2 (2021-02-03) 211 | 212 | 213 | 214 | ## 0.3.1 (2021-02-01) 215 | 216 | 217 | 218 | ## 0.2.9 (2021-01-18) 219 | 220 | 221 | 222 | ## 0.2.7 (2020-11-24) 223 | 224 | 225 | 226 | ## 0.2.6 (2020-11-09) 227 | 228 | 229 | 230 | 231 | 232 | # Changelog 233 | 234 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 235 | 236 | ### [0.3.15](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.9...v0.3.15) (2021-10-08) 237 | 238 | 239 | ### Features 240 | 241 | * disable registerDelegatedAdministrator if not using DNS setup to unlock accounts creation ([0a7213e](https://github.com/awslabs/aws-bootstrap-kit/commit/0a7213e1380a03a225e898cc2fe4c9154ce6317c)) 242 | 243 | ### [0.3.14](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.9...v0.3.14) (2021-08-03) 244 | 245 | 246 | ### Bug Fixes 247 | 248 | * Creation dependencies for Cloudtrail ([0971961](https://github.com/awslabs/aws-bootstrap-kit/commit/097196174a516b3faa0a65cdf55f27df6f9b76ee)) 249 | 250 | ### [0.3.13](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.12...v0.3.13) (2021-07-15) 251 | 252 | ### [0.3.12](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.11...v0.3.12) (2021-07-15) 253 | 254 | * bump cdk to 1.114.0 255 | 256 | ### [0.3.11](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.10...v0.3.11) (2021-07-15) 257 | 258 | * bump cdk to 1.113.0 259 | 260 | ### Bug Fixes 261 | 262 | * onUpdate and onDelete logic of custom resources ([6692f90](https://github.com/awslabs/aws-bootstrap-kit/commit/6692f905f3c9e9843a493b13a3a2f3e6063d945e)) 263 | 264 | ### [0.3.10](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.9...v0.3.10) (2021-06-04) 265 | 266 | ### Bug Fixes 267 | 268 | * bump nodejs version for lambda to 14 and cdk version to 1.107.0 to fix deprecation approaching ([338f317](https://github.com/awslabs/aws-bootstrap-kit/commit/338f317d2ace8a11626b574519ef40e5c34615b0)) 269 | 270 | ### [0.3.11](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.9...v0.3.11) (2021-06-04) 271 | 272 | ### [0.3.10](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.9...v0.3.10) (2021-06-04) 273 | 274 | ### [0.3.9](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.6...v0.3.9) (2021-04-01) 275 | 276 | 277 | ### Bug Fixes 278 | 279 | * CVE-2020-7774 ([8f2fd54](https://github.com/awslabs/aws-bootstrap-kit/commit/8f2fd54660c329872fbc3a6ca7f1349db006ae81)) 280 | 281 | ### [0.3.8](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.6...v0.3.8) (2021-03-08) 282 | 283 | ### [0.3.7](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.6...v0.3.7) (2021-03-04) 284 | 285 | ### [0.3.6](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.5...v0.3.6) (2021-02-12) 286 | 287 | ### [0.3.5](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.4...v0.3.5) (2021-02-09) 288 | 289 | ### [0.3.4](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.3...v0.3.4) (2021-02-09) 290 | 291 | ### [0.3.3](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.2...v0.3.3) (2021-02-08) 292 | 293 | ### [0.3.2](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.3.1...v0.3.2) (2021-02-03) 294 | 295 | ### [0.3.1](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.2.9...v0.3.1) (2021-02-01) 296 | 297 | ### [0.2.9](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.2.7...v0.2.9) (2021-01-18) 298 | 299 | ### [0.2.8](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.2.7...v0.2.8) (2020-11-27) 300 | 301 | ### [0.2.7](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.2.6...v0.2.7) (2020-11-24) 302 | 303 | ### 0.2.6 (2020-11-09) 304 | 305 | ### 0.2.5 (2020-10-30) 306 | 307 | ### [0.2.4](https://github.com/awslabs/aws-bootstrap-kit/compare/v0.2.3...v0.2.4) (2020-10-21) 308 | 309 | * test bump version script 310 | -------------------------------------------------------------------------------- /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 2 | 3 | If you need to customize/fix/contribute to this jsii construct here is the doc: 4 | 5 | ### TL;DR for JS/TS CDK app 6 | 7 | ``` 8 | npx projen && npx projen build && npx projen package:js 9 | ``` 10 | you end up with a tgz package into dist folder importable into your cdk app with the following lines in your package.json 11 | 12 | 13 | ``` 14 | "dependencies": { 15 | ... 16 | "aws-bootstrap-kit": "file:../../dist/js/aws-bootstrap-kit@0.2.4.jsii.tgz", 17 | ... 18 | } 19 | ``` 20 | 21 | ### init 22 | 23 | ``` 24 | npx projen 25 | ``` 26 | 27 | ### build and test 28 | 29 | The build script will compile to JSii and generate the API doc into [API.md](./API.md) 30 | ``` 31 | npx projen build 32 | ``` 33 | 34 | ``` 35 | npx projen test 36 | ``` 37 | 38 | ### Package 39 | 40 | #### Package JS only 41 | 42 | ``` 43 | npx projen package:js 44 | ``` 45 | 46 | #### All 47 | 48 | ``` 49 | npx projen package 50 | ``` 51 | 52 | ### Bump version 53 | 54 | ``` 55 | npx projen release 56 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWSBootstrapKit 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Deprecation Notice 3 | 4 | Current package is deprecated and will not be maintained anymore. The implementation has been moved to leverage the new [AWS Landing Zone Accelerator solution](https://aws.amazon.com/solutions/implementations/landing-zone-accelerator-on-aws/) ([github](https://github.com/awslabs/landing-zone-accelerator-on-aws)). 5 | 6 | 7 | # AWS Bootstrap Kit Overview [![Mentioned in Awesome CDK](https://awesome.re/mentioned-badge.svg)](https://github.com/kolomied/awesome-cdk) ![badge npm version](https://img.shields.io/npm/v/aws-bootstrap-kit/latest) 8 | 9 | 10 | This is a strongly opinionated CDK set of constructs built for companies looking to follow AWS best practices on Day 1 while setting their development and deployment environment on AWS. 11 | 12 | Let's start small but with potential for future growth without adding tech debt. 13 | 14 | ## Getting started 15 | 16 | Check our [examples repo](https://github.com/aws-samples/aws-bootstrap-kit-examples) 17 | 18 | ## Constructs 19 | 20 | As of today we expose only one global package which expose a set of stacks and constructs to help you get started properly on AWS. 21 | 22 | ## Usage 23 | 24 | 1. install 25 | 26 | ``` 27 | npm install aws-bootstrap-kit 28 | ``` 29 | 1. Check the [Examples](https://github.com/aws-samples/aws-bootstrap-kit-examples) and [API Doc](./API.md) for more details 30 | 31 | 32 | ## Contributing 33 | 34 | Check [CONTRIBUTING.md](./CONTRIBUTING.md)) 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-bootstrap-kit", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/awslabs/aws-bootstrap-kit" 6 | }, 7 | "scripts": { 8 | "build": "npx projen build", 9 | "bump": "npx projen bump", 10 | "clobber": "npx projen clobber", 11 | "compat": "npx projen compat", 12 | "compile": "npx projen compile", 13 | "default": "npx projen default", 14 | "docgen": "npx projen docgen", 15 | "eject": "npx projen eject", 16 | "eslint": "npx projen eslint", 17 | "package": "npx projen package", 18 | "package-all": "npx projen package-all", 19 | "package:js": "npx projen package:js", 20 | "package:python": "npx projen package:python", 21 | "post-compile": "npx projen post-compile", 22 | "post-upgrade": "npx projen post-upgrade", 23 | "pre-compile": "npx projen pre-compile", 24 | "release": "npx projen release", 25 | "test": "npx projen test", 26 | "test:integ": "npx projen test:integ", 27 | "test:unit": "npx projen test:unit", 28 | "test:watch": "npx projen test:watch", 29 | "unbump": "npx projen unbump", 30 | "upgrade": "npx projen upgrade", 31 | "watch": "npx projen watch", 32 | "projen": "npx projen" 33 | }, 34 | "author": { 35 | "name": "AWS EMEA SA Specialist Builders", 36 | "email": "chazalf@amazon.com", 37 | "organization": false 38 | }, 39 | "devDependencies": { 40 | "@aws-cdk/assert": "2.60.0", 41 | "@aws-cdk/cx-api": "2.60.0", 42 | "@types/aws-lambda": "^8.10.109", 43 | "@types/jest": "^27", 44 | "@types/node": "^16", 45 | "@types/sinon": "^10.0.13", 46 | "@typescript-eslint/eslint-plugin": "^5", 47 | "@typescript-eslint/parser": "^5", 48 | "aws-cdk": "2.60.0", 49 | "aws-cdk-lib": "2.60.0", 50 | "aws-sdk-mock": "^5.8.0", 51 | "cdk-assets": "2.60.0", 52 | "constructs": "10.0.5", 53 | "eslint": "^8", 54 | "eslint-import-resolver-node": "^0.3.7", 55 | "eslint-import-resolver-typescript": "^3.5.3", 56 | "eslint-plugin-import": "^2.27.4", 57 | "jest": "^27", 58 | "jest-junit": "^13", 59 | "jest-runner-groups": "^2.2.0", 60 | "jsii": "^1.73.0", 61 | "jsii-diff": "^1.73.0", 62 | "jsii-docgen": "^7.0.202", 63 | "jsii-pacmak": "^1.73.0", 64 | "json-schema": "^0.4.0", 65 | "npm-check-updates": "^16", 66 | "projen": "^0.66.13", 67 | "promptly": "^3.2.0", 68 | "sinon": "^15.0.1", 69 | "standard-version": "^9", 70 | "ts-jest": "^27", 71 | "typescript": "^4.9.4" 72 | }, 73 | "peerDependencies": { 74 | "aws-cdk-lib": "^2.60.0", 75 | "constructs": "^10.0.5" 76 | }, 77 | "keywords": [ 78 | "cdk" 79 | ], 80 | "engines": { 81 | "node": ">= 16.0.0" 82 | }, 83 | "main": "lib/index.js", 84 | "license": "Apache-2.0", 85 | "version": "0.0.0", 86 | "jest": { 87 | "runner": "groups", 88 | "testMatch": [ 89 | "/src/**/__tests__/**/*.ts?(x)", 90 | "/(test|src)/**/*(*.)@(spec|test).ts?(x)" 91 | ], 92 | "clearMocks": true, 93 | "collectCoverage": true, 94 | "coverageReporters": [ 95 | "json", 96 | "lcov", 97 | "clover", 98 | "cobertura", 99 | "text" 100 | ], 101 | "coverageDirectory": "coverage", 102 | "coveragePathIgnorePatterns": [ 103 | "/node_modules/" 104 | ], 105 | "testPathIgnorePatterns": [ 106 | "/node_modules/" 107 | ], 108 | "watchPathIgnorePatterns": [ 109 | "/node_modules/" 110 | ], 111 | "reporters": [ 112 | "default", 113 | [ 114 | "jest-junit", 115 | { 116 | "outputDirectory": "test-reports" 117 | } 118 | ] 119 | ], 120 | "preset": "ts-jest", 121 | "globals": { 122 | "ts-jest": { 123 | "tsconfig": "tsconfig.dev.json" 124 | } 125 | } 126 | }, 127 | "types": "lib/index.d.ts", 128 | "stability": "stable", 129 | "jsii": { 130 | "outdir": "dist", 131 | "targets": { 132 | "python": { 133 | "distName": "aws_ssabuilders.aws_bootstrap_kit", 134 | "module": "aws_ssabuilders.aws_bootstrap_kit" 135 | } 136 | }, 137 | "tsc": { 138 | "outDir": "lib", 139 | "rootDir": "src" 140 | } 141 | }, 142 | "resolutions": { 143 | "@types/prettier": "2.6.0", 144 | "@types/babel__traverse": "7.18.2" 145 | }, 146 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 147 | } 148 | -------------------------------------------------------------------------------- /rfc/README.md: -------------------------------------------------------------------------------- 1 | # AWS Bootstrap Kit RFC -------------------------------------------------------------------------------- /src/account-handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | import type { 19 | IsCompleteRequest, 20 | IsCompleteResponse, 21 | OnEventResponse, 22 | } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 23 | 24 | // eslint-disable-line import/no-extraneous-dependencies 25 | import { Organizations } from 'aws-sdk'; 26 | 27 | /** 28 | * A function capable of creating an account into an AWS Organisation 29 | * @param event An event with the following ResourceProperties: Email (coresponding to the account email) and AccountName (corresponding to the account name) 30 | * @returns Returns a PhysicalResourceId corresponding to the CreateAccount request's id necessary to check the status of the creation 31 | */ 32 | 33 | export async function onEventHandler(event: any): Promise { 34 | console.log('Event: %j', event); 35 | 36 | switch (event.RequestType) { 37 | case 'Create': 38 | const awsOrganizationsClient = new Organizations({ region: 'us-east-1' }); 39 | try { 40 | const data = await awsOrganizationsClient 41 | .createAccount({ 42 | Email: event.ResourceProperties.Email, 43 | AccountName: event.ResourceProperties.AccountName, 44 | }) 45 | .promise(); 46 | console.log('create account: %j', data); 47 | return { PhysicalResourceId: data.CreateAccountStatus?.Id }; 48 | } catch (error) { 49 | throw new Error(`Failed to create account: ${error}`); 50 | } 51 | case 'Delete': // only called if the removalPolicy is DESTROY 52 | throw new Error(`Cannot delete account '${event.PhysicalResourceId}'. See https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_remove.html`); 53 | default: 54 | // just return the resource (we cannot update or delete an account) 55 | return { 56 | PhysicalResourceId: event.PhysicalResourceId, 57 | ResourceProperties: event.ResourceProperties, 58 | }; 59 | } 60 | } 61 | 62 | /** 63 | * A function capable to check if an account creation request has been completed 64 | * @param event An event containing a PhysicalResourceId corresponding to a CreateAccountRequestId 65 | * @returns A payload containing the IsComplete Flag requested by cdk Custom Resource fwk to figure out if the resource has been created or failed to be or if it needs to retry 66 | */ 67 | export async function isCompleteHandler( 68 | event: IsCompleteRequest, 69 | ): Promise { 70 | console.log('Event: %j', event); 71 | 72 | if (!event.PhysicalResourceId) { 73 | throw new Error('Missing PhysicalResourceId parameter.'); 74 | } 75 | 76 | const awsOrganizationsClient = new Organizations({ region: 'us-east-1' }); 77 | 78 | const describeCreateAccountStatusParams: Organizations.DescribeCreateAccountStatusRequest = 79 | { CreateAccountRequestId: event.PhysicalResourceId }; 80 | const data: Organizations.DescribeCreateAccountStatusResponse = 81 | await awsOrganizationsClient 82 | .describeCreateAccountStatus(describeCreateAccountStatusParams) 83 | .promise(); 84 | 85 | console.log('Describe account: %j', data); 86 | 87 | const CreateAccountStatus = data.CreateAccountStatus?.State; 88 | const AccountId = data.CreateAccountStatus?.AccountId; 89 | 90 | switch (event.RequestType) { 91 | case 'Create': 92 | if (CreateAccountStatus === 'FAILED') { 93 | throw new Error( 94 | `Error creating the account ${data.CreateAccountStatus?.AccountName}, cause: ${data.CreateAccountStatus?.FailureReason}`, 95 | ); 96 | } 97 | return { 98 | IsComplete: CreateAccountStatus === 'SUCCEEDED', 99 | Data: { AccountId: AccountId }, 100 | }; 101 | default: 102 | return { 103 | IsComplete: CreateAccountStatus === 'SUCCEEDED', 104 | Data: { AccountId: AccountId }, 105 | }; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/account-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import { Duration, NestedStack, Stack } from 'aws-cdk-lib'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 21 | import * as cr from 'aws-cdk-lib/custom-resources'; 22 | import { Construct } from 'constructs'; 23 | 24 | 25 | /** 26 | * A Custom Resource provider capable of creating AWS Accounts 27 | */ 28 | export class AccountProvider extends NestedStack { 29 | /** 30 | * Creates a stack-singleton resource provider nested stack. 31 | */ 32 | public static getOrCreate(scope: Construct) { 33 | const stack = Stack.of(scope); 34 | const uid = '@aws-cdk/aws-bootstrap-kit.AccountProvider'; 35 | return stack.node.tryFindChild(uid) as AccountProvider || new AccountProvider(stack, uid); 36 | } 37 | 38 | /** 39 | * The custom resource provider. 40 | */ 41 | public readonly provider: cr.Provider; 42 | 43 | /** 44 | * The onEvent handler 45 | */ 46 | public readonly onEventHandler: lambda.Function; 47 | 48 | /** 49 | * The isComplete handler 50 | */ 51 | public readonly isCompleteHandler: lambda.Function; 52 | 53 | private constructor(scope: Construct, id: string) { 54 | super(scope, id); 55 | 56 | const code = lambda.Code.fromAsset(path.join(__dirname, 'account-handler')); 57 | 58 | // Issues UpdateTable API calls 59 | this.onEventHandler = new lambda.Function(this, 'OnEventHandler', { 60 | code, 61 | runtime: lambda.Runtime.NODEJS_14_X, 62 | handler: 'index.onEventHandler', 63 | timeout: Duration.minutes(5), 64 | }); 65 | 66 | this.onEventHandler.addToRolePolicy( 67 | new iam.PolicyStatement({ 68 | actions: [ 69 | 'organizations:CreateAccount', 70 | 'organizations:TagResource', 71 | ], 72 | resources: ['*'], 73 | }), 74 | ); 75 | 76 | // Checks if account is ready 77 | this.isCompleteHandler = new lambda.Function(this, 'IsCompleteHandler', { 78 | code, 79 | runtime: lambda.Runtime.NODEJS_14_X, 80 | handler: 'index.isCompleteHandler', 81 | timeout: Duration.seconds(30), 82 | }); 83 | 84 | this.isCompleteHandler.addToRolePolicy( 85 | new iam.PolicyStatement({ 86 | actions: [ 87 | 'organizations:CreateAccount', 88 | 'organizations:DescribeCreateAccountStatus', 89 | 'organizations:TagResource', 90 | ], 91 | resources: ['*'], 92 | }), 93 | ); 94 | 95 | this.provider = new cr.Provider(this, 'AccountProvider', { 96 | onEventHandler: this.onEventHandler, 97 | isCompleteHandler: this.isCompleteHandler, 98 | queryInterval: Duration.seconds(10), 99 | }); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/account.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as core from 'aws-cdk-lib'; 18 | import { RemovalPolicy } from 'aws-cdk-lib'; 19 | import * as ssm from 'aws-cdk-lib/aws-ssm'; 20 | import * as cr from 'aws-cdk-lib/custom-resources'; 21 | import { Construct } from 'constructs'; 22 | import { AccountProvider } from './account-provider'; 23 | 24 | /** 25 | * Properties of an AWS account 26 | */ 27 | export interface IAccountProps { 28 | /** 29 | * The email to use to create the AWS account 30 | */ 31 | email: string; 32 | /** 33 | * The name of the AWS Account 34 | */ 35 | name: string; 36 | /** 37 | * The account type 38 | */ 39 | type?: AccountType; 40 | /** 41 | * The (optional) Stage name to be used in CI/CD pipeline 42 | */ 43 | stageName?: string; 44 | /** 45 | * The (optional) Stage deployment order 46 | */ 47 | stageOrder?: number; 48 | /** 49 | * List of your services that will be hosted in this account. Set it to [ALL] if you don't plan to have dedicated account for each service. 50 | */ 51 | hostedServices?: string[]; 52 | /** 53 | * The potential Organizational Unit Id the account should be placed in 54 | */ 55 | parentOrganizationalUnitId?: string; 56 | /** 57 | * The potential Organizational Unit Name the account should be placed in 58 | */ 59 | parentOrganizationalUnitName?: string; 60 | /** 61 | * The AWS account Id 62 | */ 63 | id?: string; 64 | /** 65 | * RemovalPolicy of the account. See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html#aws-attribute-deletionpolicy-options 66 | * 67 | * @default RemovalPolicy.RETAIN 68 | */ 69 | removalPolicy?: RemovalPolicy; 70 | } 71 | 72 | /** 73 | * The type of the AWS account 74 | **/ 75 | export enum AccountType { 76 | /** 77 | * The account used to deploy CI/CD pipelines (See [here](https://cs.github.com/?scopeName=bk&scope=repo%3Aawslabs%2Faws-bootstrap-kit&q=AccountType.CICD) for internal usage). 78 | */ 79 | CICD = 'CICD', 80 | /** 81 | * Accounts which will be used to deploy Stage environments (staging/prod ...). (See [here](https://cs.github.com/?scopeName=bk&scope=repo%3Aawslabs%2Faws-bootstrap-kit&q=AccountType.STAGE) for internal usage) 82 | **/ 83 | STAGE = 'STAGE', 84 | /** 85 | * Sandbox accounts dedicated to developers work. 86 | */ 87 | PLAYGROUND = 'PLAYGROUND' 88 | } 89 | 90 | /** 91 | * An AWS Account 92 | */ 93 | export class Account extends Construct { 94 | /** 95 | * Constructor 96 | * 97 | * @param scope The parent Construct instantiating this account 98 | * @param id This instance name 99 | * @param accountProps Account properties 100 | */ 101 | readonly accountName: string; 102 | readonly accountId: string; 103 | readonly accountStageName?: string; 104 | readonly accountStageOrder?: number; 105 | 106 | constructor(scope: Construct, id: string, accountProps: IAccountProps) { 107 | super(scope, id); 108 | 109 | const accountProvider = AccountProvider.getOrCreate(this); 110 | 111 | let existingAccount = false; 112 | let accountId = accountProps.id; 113 | let hostedServices = accountProps.hostedServices ? accountProps.hostedServices.join(':') : undefined; 114 | 115 | // do not create account if we reuse one 116 | let account; 117 | if (!accountId) { 118 | account = new core.CustomResource( 119 | this, 120 | `Account-${accountProps.name}`, 121 | { 122 | serviceToken: accountProvider.provider.serviceToken, 123 | resourceType: 'Custom::AccountCreation', 124 | properties: { 125 | Email: accountProps.email, 126 | AccountName: accountProps.name, 127 | }, 128 | removalPolicy: accountProps.removalPolicy || RemovalPolicy.RETAIN, 129 | }, 130 | ); 131 | accountId = account.getAtt('AccountId').toString(); 132 | accountProps.id = accountId; 133 | } else { 134 | existingAccount = true; 135 | 136 | // retrieve existing account information (actual name and email) to update accountProps 137 | account = new cr.AwsCustomResource(this, 'ExistingAccountCustomResource', { 138 | onUpdate: { 139 | service: 'Organizations', 140 | action: 'describeAccount', 141 | physicalResourceId: cr.PhysicalResourceId.fromResponse( 142 | 'Account.Id', 143 | ), 144 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 145 | parameters: { 146 | AccountId: accountId, 147 | }, 148 | }, 149 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 150 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 151 | }), 152 | }); 153 | 154 | accountProps.name = account.getResponseField('Account.Name'); 155 | accountProps.email = account.getResponseField('Account.Email'); 156 | } 157 | 158 | // add tags 159 | const tags: { Key: string; Value: any }[] = []; 160 | tags.push({ Key: 'Email', Value: accountProps.email }); 161 | tags.push({ Key: 'AccountName', Value: accountProps.name }); 162 | if (accountProps.type != null) tags.push({ Key: 'AccountType', Value: accountProps.type }); 163 | if (accountProps.stageName != null) tags.push({ Key: 'StageName', Value: accountProps.stageName }); 164 | if (accountProps.stageOrder != null) tags.push({ Key: 'StageOrder', Value: accountProps.stageOrder.toString() }); 165 | if (hostedServices != null) tags.push({ Key: 'HostedServices', Value: hostedServices }); 166 | const tagAccount = { 167 | service: 'Organizations', 168 | action: 'tagResource', 169 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 170 | physicalResourceId: cr.PhysicalResourceId.of(`tags-${accountId}`), 171 | parameters: { 172 | ResourceId: accountId, 173 | Tags: tags, 174 | }, 175 | }; 176 | new cr.AwsCustomResource(this, 'TagAccountCustomResource', { 177 | onCreate: tagAccount, 178 | onUpdate: tagAccount, 179 | onDelete: { 180 | service: 'Organizations', 181 | action: 'untagResource', 182 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 183 | parameters: { 184 | ResourceId: accountId, 185 | TagKeys: tags.map(t => t.Key), 186 | }, 187 | }, 188 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 189 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 190 | }), 191 | }); 192 | 193 | this.accountName = accountProps.name; 194 | this.accountId = accountId; 195 | this.accountStageName = accountProps.stageName; 196 | this.accountStageOrder = accountProps.stageOrder; 197 | 198 | let ssmParam = new ssm.StringParameter(this, existingAccount?`${accountId}-AccountDetails`:`${accountProps.name}-AccountDetails`, { 199 | description: `Details of ${accountProps.name}`, 200 | parameterName: `/accounts/${accountProps.name}`, 201 | simpleName: false, 202 | stringValue: JSON.stringify(accountProps), 203 | }); 204 | ssmParam.node.addDependency(account); 205 | 206 | if (accountProps.parentOrganizationalUnitId) { 207 | let parent = new cr.AwsCustomResource(this, 'ListParentsCustomResource', { 208 | onUpdate: { 209 | service: 'Organizations', 210 | action: 'listParents', 211 | physicalResourceId: cr.PhysicalResourceId.of(`${accountProps.parentOrganizationalUnitId}-${accountId}`), 212 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 213 | parameters: { 214 | ChildId: accountId, 215 | }, 216 | }, 217 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 218 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 219 | }), 220 | }); 221 | 222 | new cr.AwsCustomResource(this, 'MoveAccountCustomResource', 223 | { 224 | onUpdate: { 225 | service: 'Organizations', 226 | action: 'moveAccount', 227 | physicalResourceId: cr.PhysicalResourceId.of(`move-${accountId}`), 228 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 229 | parameters: { 230 | AccountId: accountId, 231 | DestinationParentId: accountProps.parentOrganizationalUnitId, 232 | SourceParentId: parent.getResponseField('Parents.0.Id'), 233 | }, 234 | ignoreErrorCodesMatching: 'DuplicateAccountException', // ignore if account is already there 235 | }, 236 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 237 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 238 | }), 239 | }, 240 | ); 241 | 242 | 243 | // Enabling Organizations listAccounts call for auto resolution of stages and DNS accounts Ids and Names 244 | if (accountProps.type === AccountType.CICD) { 245 | this.registerAsDelegatedAdministrator(accountId, 'ssm.amazonaws.com'); 246 | } else { 247 | // Switching to another principal to workaround the max number of delegated administrators (which is set to 3 by default). 248 | const needsToBeDelegatedForDNSZOneNameResolution = this.node.tryGetContext('domain_name') ?? false; 249 | if (needsToBeDelegatedForDNSZOneNameResolution) {this.registerAsDelegatedAdministrator(accountId, 'config-multiaccountsetup.amazonaws.com');} 250 | } 251 | 252 | } 253 | } 254 | 255 | registerAsDelegatedAdministrator(accountId: string, servicePrincipal: string) { 256 | new cr.AwsCustomResource(this, 257 | 'registerDelegatedAdministrator', 258 | { 259 | onCreate: { 260 | service: 'Organizations', 261 | action: 'registerDelegatedAdministrator', // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Organizations.html#registerDelegatedAdministrator-property 262 | physicalResourceId: cr.PhysicalResourceId.of('registerDelegatedAdministrator'), 263 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 264 | parameters: { 265 | AccountId: accountId, 266 | ServicePrincipal: servicePrincipal, 267 | }, 268 | }, 269 | onDelete: { 270 | service: 'Organizations', 271 | action: 'deregisterDelegatedAdministrator', // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Organizations.html#deregisterDelegatedAdministrator-property 272 | physicalResourceId: cr.PhysicalResourceId.of('registerDelegatedAdministrator'), 273 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 274 | parameters: { 275 | AccountId: accountId, 276 | ServicePrincipal: servicePrincipal, 277 | }, 278 | }, 279 | installLatestAwsSdk: false, 280 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls( 281 | { 282 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 283 | }, 284 | ), 285 | }, 286 | ); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/aws-config-recorder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as cfg from 'aws-cdk-lib/aws-config'; 18 | import * as iam from 'aws-cdk-lib/aws-iam'; 19 | import * as s3 from 'aws-cdk-lib/aws-s3'; 20 | import { Construct } from 'constructs'; 21 | 22 | 23 | // from https://github.com/aws-samples/aws-startup-blueprint/blob/mainline/lib/aws-config-packs.ts 24 | export class ConfigRecorder extends Construct { 25 | 26 | constructor(scope: Construct, id: string) { 27 | super(scope, id); 28 | 29 | 30 | const configBucket = new s3.Bucket(this, 'ConfigBucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL }); 31 | 32 | configBucket.addToResourcePolicy( 33 | new iam.PolicyStatement({ 34 | effect: iam.Effect.DENY, 35 | actions: ['*'], 36 | principals: [new iam.AnyPrincipal()], 37 | resources: [configBucket.bucketArn, configBucket.arnForObjects('*')], 38 | conditions: { 39 | Bool: { 40 | 'aws:SecureTransport': false, 41 | }, 42 | }, 43 | }), 44 | ); 45 | 46 | // Attach AWSConfigBucketPermissionsCheck to config bucket 47 | configBucket.addToResourcePolicy( 48 | new iam.PolicyStatement({ 49 | effect: iam.Effect.ALLOW, 50 | principals: [new iam.ServicePrincipal('config.amazonaws.com')], 51 | resources: [configBucket.bucketArn], 52 | actions: ['s3:GetBucketAcl'], 53 | }), 54 | ); 55 | 56 | // Attach AWSConfigBucketDelivery to config bucket 57 | configBucket.addToResourcePolicy( 58 | new iam.PolicyStatement({ 59 | effect: iam.Effect.ALLOW, 60 | principals: [new iam.ServicePrincipal('config.amazonaws.com')], 61 | resources: [`${configBucket.bucketArn}/*`], 62 | actions: ['s3:PutObject'], 63 | conditions: { 64 | StringEquals: { 65 | 's3:x-amz-acl': 'bucket-owner-full-control', 66 | }, 67 | }, 68 | }), 69 | ); 70 | 71 | new cfg.CfnDeliveryChannel(this, 'ConfigDeliveryChannel', { 72 | s3BucketName: configBucket.bucketName, 73 | name: 'ConfigDeliveryChannel', 74 | }); 75 | 76 | 77 | const configRole = new iam.Role(this, 'ConfigRecorderRole', { 78 | assumedBy: new iam.ServicePrincipal('config.amazonaws.com'), 79 | managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWS_ConfigRole')], 80 | }); 81 | 82 | new cfg.CfnConfigurationRecorder(this, 'ConfigRecorder', { 83 | name: 'BlueprintConfigRecorder', 84 | roleArn: configRole.roleArn, 85 | recordingGroup: { 86 | resourceTypes: [ 87 | 'AWS::IAM::User', 88 | ], 89 | }, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/aws-organizations-stack.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; 18 | import { Construct, IDependable } from 'constructs'; 19 | import { Account, AccountType } from './account'; 20 | import { RootDns } from './dns'; 21 | import { Organization } from './organization'; 22 | import { OrganizationTrail } from './organization-trail'; 23 | import { OrganizationalUnit } from './organizational-unit'; 24 | import { SecureRootUser } from './secure-root-user'; 25 | import { ValidateEmail } from './validate-email'; 26 | 27 | const version = require('../package.json').version; 28 | 29 | /** 30 | * AWS Account input details 31 | */ 32 | export interface AccountSpec { 33 | 34 | /** 35 | * The name of the AWS account 36 | */ 37 | readonly name: string; 38 | /** 39 | * The (optional) id of the account to reuse, instead of creating a new account 40 | */ 41 | readonly existingAccountId?: string; 42 | /** 43 | * The email associated to the AWS account 44 | */ 45 | readonly email?: string; 46 | /** 47 | * The account type 48 | */ 49 | readonly type?: AccountType; 50 | /** 51 | * The (optional) Stage name to be used in CI/CD pipeline 52 | */ 53 | readonly stageName?: string; 54 | /** 55 | * The (optional) Stage deployment order 56 | */ 57 | readonly stageOrder?: number; 58 | /** 59 | * List of your services that will be hosted in this account. Set it to [ALL] if you don't plan to have dedicated account for each service. 60 | */ 61 | readonly hostedServices?: string[]; 62 | /** 63 | * RemovalPolicy of the account (wether it must be retained or destroyed). 64 | * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html#aws-attribute-deletionpolicy-options 65 | * 66 | * As an account cannot be deleted, RETAIN is the default value. 67 | * 68 | * If you choose DESTROY instead (default behavior of CloudFormation), the stack deletion will fail and 69 | * you will have to manually remove the account from the organization before retrying to delete the stack: 70 | * https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_remove.html 71 | * 72 | * Note that existing accounts (when using `existingAccountId`) are retained whatever the removalPolicy is. 73 | * 74 | * @default RemovalPolicy.RETAIN 75 | */ 76 | readonly removalPolicy?: RemovalPolicy; 77 | } 78 | 79 | /** 80 | * Organizational Unit Input details 81 | */ 82 | export interface OUSpec { 83 | /** 84 | * Name of the Organizational Unit 85 | */ 86 | readonly name: string; 87 | 88 | /** 89 | * Accounts' specification inside in this Organizational Unit 90 | */ 91 | readonly accounts?: AccountSpec[]; 92 | 93 | /** 94 | * Specification of sub Organizational Unit 95 | */ 96 | readonly nestedOU?: OUSpec[]; 97 | } 98 | 99 | /** 100 | * Properties for AWS SDLC Organizations Stack 101 | * @experimental 102 | */ 103 | export interface AwsOrganizationsStackProps extends StackProps { 104 | 105 | /** 106 | * Email address of the Root account 107 | */ 108 | readonly email: string; 109 | 110 | /** 111 | * Specification of the sub Organizational Unit 112 | */ 113 | readonly nestedOU: OUSpec[]; 114 | 115 | /** 116 | * The main DNS domain name to manage 117 | */ 118 | readonly rootHostedZoneDNSName?: string; 119 | 120 | /** 121 | * The (optional) existing root hosted zone id to use instead of creating one 122 | */ 123 | readonly existingRootHostedZoneId?: string; 124 | 125 | /** 126 | * A boolean used to decide if domain should be requested through this delpoyment or if already registered through a third party 127 | */ 128 | readonly thirdPartyProviderDNSUsed?: boolean; 129 | 130 | /** 131 | * Enable Email Verification Process 132 | */ 133 | readonly forceEmailVerification?: boolean; 134 | } 135 | 136 | /** 137 | * A Stack creating the Software Development Life Cycle (SDLC) Organization 138 | */ 139 | export class AwsOrganizationsStack extends Stack { 140 | 141 | private readonly emailPrefix?: string; 142 | private readonly domain?: string; 143 | private readonly stageAccounts: Account[] = []; 144 | public readonly rootDns?: RootDns; 145 | 146 | private createOrganizationTree(oUSpec: OUSpec, parentId: string, previousSequentialConstruct: IDependable): IDependable { 147 | 148 | let organizationalUnit = new OrganizationalUnit(this, `${oUSpec.name}-OU`, { Name: oUSpec.name, ParentId: parentId }); 149 | //adding an explicit dependency as CloudFormation won't infer that Organization, Organizational Units and Accounts must be created or modified sequentially 150 | organizationalUnit.node.addDependency(previousSequentialConstruct); 151 | 152 | previousSequentialConstruct = organizationalUnit; 153 | 154 | oUSpec.accounts?.forEach(accountSpec => { 155 | let accountEmail: string; 156 | if (accountSpec.email) { 157 | accountEmail = accountSpec.email; 158 | } else if (this.emailPrefix && this.domain) { 159 | accountEmail = `${this.emailPrefix}+${accountSpec.name}-${Stack.of(this).account}@${this.domain}`; 160 | } else { 161 | throw new Error(`Master account email must be provided or an account email for account ${accountSpec.name}`); 162 | } 163 | 164 | let account = new Account(this, accountSpec.name, { 165 | email: accountEmail, 166 | name: accountSpec.name, 167 | parentOrganizationalUnitId: organizationalUnit.id, 168 | type: accountSpec.type, 169 | stageName: accountSpec.stageName, 170 | stageOrder: accountSpec.stageOrder, 171 | hostedServices: accountSpec.hostedServices, 172 | id: accountSpec.existingAccountId, 173 | removalPolicy: accountSpec.removalPolicy, 174 | }); 175 | 176 | // Adding an explicit dependency as CloudFormation won't infer that Organization, Organizational Units and Accounts must be created or modified sequentially 177 | account.node.addDependency(previousSequentialConstruct); 178 | previousSequentialConstruct = account; 179 | 180 | // Building stageAccounts array to be used for DNS delegation system 181 | if (accountSpec.type == AccountType.STAGE) { 182 | this.stageAccounts.push(account); 183 | } 184 | }); 185 | 186 | oUSpec.nestedOU?.forEach(nestedOU => { 187 | previousSequentialConstruct = this.createOrganizationTree(nestedOU, organizationalUnit.id, previousSequentialConstruct); 188 | }); 189 | 190 | return previousSequentialConstruct; 191 | } 192 | 193 | constructor(scope: Construct, id: string, props: AwsOrganizationsStackProps) { 194 | super(scope, id, { description: `Software development Landing Zone (uksb-1r7an8o45) (version:${version})`, ...props }); 195 | const { email, nestedOU, forceEmailVerification = true } = props; 196 | 197 | if (nestedOU.length > 0) { 198 | let org = new Organization(this, 'Organization'); 199 | if (email) { 200 | this.emailPrefix = email.split('@', 2)[0]; 201 | this.domain = email.split('@', 2)[1]; 202 | 203 | if (forceEmailVerification) { 204 | const validateEmail = new ValidateEmail(this, 'EmailValidation', { email }); 205 | org.node.addDependency(validateEmail); 206 | } 207 | } 208 | 209 | let orgTrail = new OrganizationTrail(this, 'OrganizationTrail', { OrganizationId: org.id }); 210 | orgTrail.node.addDependency(org); 211 | 212 | let previousSequentialConstruct: IDependable = orgTrail; 213 | 214 | nestedOU.forEach(nestedOU => { 215 | previousSequentialConstruct = this.createOrganizationTree(nestedOU, org.rootId, previousSequentialConstruct); 216 | }); 217 | } 218 | 219 | if (props.rootHostedZoneDNSName) { 220 | this.rootDns = new RootDns(this, 'RootDNS', { 221 | stagesAccounts: this.stageAccounts, 222 | rootHostedZoneDNSName: props.rootHostedZoneDNSName, 223 | existingRootHostedZoneId: props.existingRootHostedZoneId, 224 | thirdPartyProviderDNSUsed: props.thirdPartyProviderDNSUsed?props.thirdPartyProviderDNSUsed:true, 225 | }); 226 | } 227 | 228 | new SecureRootUser(this, 'SecureRootUser', email); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/dns-stack.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | import { RootDns, RootDnsProps } from './dns'; 5 | 6 | /** 7 | * Properties of the Root DNS Stack 8 | */ 9 | export interface RootDNSStackProps extends cdk.StackProps { 10 | /** 11 | * Properties of the Root DNS Construct 12 | */ 13 | readonly rootDnsProps: RootDnsProps; 14 | } 15 | 16 | /** 17 | * A Stack creating a root DNS Zone with subzone delegation capabilities 18 | */ 19 | export class RootDNSStack extends cdk.Stack { 20 | public rootDns: RootDns; 21 | 22 | constructor(scope: Construct, id: string, props: RootDNSStackProps) { 23 | super(scope, id, props); 24 | 25 | this.rootDns = new RootDns(this, 'RootDNSZone', props.rootDnsProps); 26 | } 27 | } -------------------------------------------------------------------------------- /src/dns.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as iam from 'aws-cdk-lib/aws-iam'; 4 | import * as route53 from 'aws-cdk-lib/aws-route53'; 5 | import { RecordTarget } from 'aws-cdk-lib/aws-route53'; 6 | import { Construct } from 'constructs'; 7 | import { Account } from './account'; 8 | import * as utils from './dns/delegation-record-handler/utils'; 9 | 10 | /** 11 | * Properties for RootDns 12 | */ 13 | export interface RootDnsProps { 14 | /** 15 | * The stages Accounts taht will need their subzone delegation 16 | */ 17 | readonly stagesAccounts: Account[]; 18 | /** 19 | * The top level domain name 20 | */ 21 | readonly rootHostedZoneDNSName: string; 22 | 23 | /** 24 | * The (optional) existing root hosted zone id to use instead of creating one 25 | */ 26 | readonly existingRootHostedZoneId?: string; 27 | 28 | /** 29 | * A boolean indicating if Domain name has already been registered to a third party or if you want this contruct to create it (the latter is not yet supported) 30 | */ 31 | readonly thirdPartyProviderDNSUsed?: boolean; 32 | } 33 | 34 | /** 35 | * A class creating the main hosted zone and a role assumable by stages account to be able to set sub domain delegation 36 | */ 37 | export class RootDns extends Construct { 38 | public readonly rootHostedZone: route53.IHostedZone; 39 | public readonly stagesHostedZones: route53.HostedZone[] = []; 40 | 41 | constructor(scope: Construct, id: string, props: RootDnsProps) { 42 | super(scope, id); 43 | this.rootHostedZone = this.createRootHostedZone(props); 44 | 45 | for (const accountIndex in props.stagesAccounts) { 46 | const account = props.stagesAccounts[accountIndex]; 47 | const stageSubZone = this.createStageSubZone( 48 | account, 49 | props.rootHostedZoneDNSName, 50 | ); 51 | this.stagesHostedZones.push(stageSubZone); 52 | this.createDNSAutoUpdateRole(account, stageSubZone); 53 | if (stageSubZone.hostedZoneNameServers) { 54 | new route53.RecordSet( 55 | this, 56 | `${account.accountName}SubZoneDelegationNSRecord`, 57 | { 58 | recordType: route53.RecordType.NS, 59 | target: RecordTarget.fromValues(...stageSubZone.hostedZoneNameServers?stageSubZone.hostedZoneNameServers:''), 60 | recordName: stageSubZone.zoneName, 61 | zone: this.rootHostedZone, 62 | }, 63 | ); 64 | } 65 | } 66 | 67 | if ( 68 | props.thirdPartyProviderDNSUsed && 69 | this.rootHostedZone.hostedZoneNameServers 70 | ) { 71 | new cdk.CfnOutput(this, 'NS records', { 72 | value: cdk.Fn.join(',', this.rootHostedZone.hostedZoneNameServers), 73 | }); 74 | } else { 75 | if (!props.existingRootHostedZoneId) { 76 | throw new Error('Creation of DNS domain is not yet supported'); 77 | // TODO: implement call to https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Route53Domains.html#registerDomain-property 78 | } 79 | } 80 | } 81 | 82 | createStageSubZone( 83 | account: Account, 84 | rootHostedZoneDNSName: string, 85 | ): route53.HostedZone { 86 | const subDomainPrefix = utils.getSubdomainPrefix(account.accountName, account.accountStageName); 87 | return new route53.HostedZone(this, `${subDomainPrefix}StageSubZone`, { 88 | zoneName: `${subDomainPrefix}.${rootHostedZoneDNSName}`, 89 | }); 90 | } 91 | 92 | createDNSAutoUpdateRole( 93 | account: Account, 94 | stageSubZone: route53.HostedZone, 95 | ) { 96 | const dnsAutoUpdateRole = new iam.Role(this, stageSubZone.zoneName, { 97 | assumedBy: new iam.AccountPrincipal(account.accountId), 98 | roleName: utils.getDNSUpdateRoleNameFromSubZoneName(stageSubZone.zoneName), 99 | }); 100 | 101 | dnsAutoUpdateRole.addToPolicy( 102 | new iam.PolicyStatement({ 103 | resources: [stageSubZone.hostedZoneArn], 104 | actions: [ 105 | 'route53:GetHostedZone', 106 | 'route53:ChangeResourceRecordSets', 107 | 'route53:TestDNSAnswer', 108 | ], 109 | }), 110 | ); 111 | dnsAutoUpdateRole.addToPolicy( 112 | new iam.PolicyStatement({ 113 | resources: ['*'], 114 | actions: [ 115 | 'route53:ListHostedZonesByName', 116 | ], 117 | }), 118 | ); 119 | return dnsAutoUpdateRole; 120 | } 121 | 122 | createRootHostedZone(props: RootDnsProps) { 123 | if (!props.existingRootHostedZoneId) { 124 | return new route53.HostedZone(this, 'RootHostedZone', { 125 | zoneName: props.rootHostedZoneDNSName, 126 | }); 127 | } else { 128 | return route53.HostedZone.fromHostedZoneAttributes(this, 'RootHostedZone', { 129 | zoneName: props.rootHostedZoneDNSName, 130 | hostedZoneId: props.existingRootHostedZoneId, 131 | }); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/dns/cross-account-dns-delegator.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as core from 'aws-cdk-lib'; 3 | import * as route53 from 'aws-cdk-lib/aws-route53'; 4 | import { Construct } from 'constructs'; 5 | import { CrossAccountZoneDelegationRecord } from './cross-account-zone-delegation-record'; 6 | 7 | /** 8 | * Properties to create delegated subzone of a zone hosted in a different account 9 | * 10 | */ 11 | export interface ICrossAccountDNSDelegatorProps { 12 | /** 13 | * The Account hosting the parent zone 14 | * Optional since can be resolved if the system has been setup with aws-bootstrap-kit 15 | */ 16 | targetAccount?: string; 17 | /** 18 | * The role to Assume in the parent zone's account which has permissions to update the parent zone 19 | * Optional since can be resolved if the system has been setup with aws-bootstrap-kit 20 | */ 21 | targetRoleToAssume?: string; 22 | /** 23 | * The parent zone Id to add the sub zone delegation NS record to 24 | * Optional since can be resolved if the system has been setup with aws-bootstrap-kit 25 | */ 26 | targetHostedZoneId?: string; 27 | /** 28 | * The sub zone name to be created 29 | */ 30 | zoneName: string; 31 | } 32 | 33 | /** 34 | * TODO: propose this to fix https://github.com/aws/aws-cdk/issues/8776 35 | * High-level construct that creates: 36 | * 1. A public hosted zone in the current account 37 | * 2. A record name in the hosted zone id of target account 38 | * 39 | * Usage: 40 | * Create a role with the following permission: 41 | * { 42 | * "Sid": "VisualEditor0", 43 | * "Effect": "Allow", 44 | * "Action": [ 45 | * "route53:GetHostedZone", 46 | * "route53:ChangeResourceRecordSets" 47 | * ], 48 | * "Resource": "arn:aws:route53:::hostedzone/ZXXXXXXXXX" 49 | * } 50 | * 51 | * Then use the construct like this: 52 | * 53 | * const crossAccountDNSDelegatorProps: ICrossAccountDNSDelegatorProps = { 54 | * targetAccount: '1234567890', 55 | * targetRoleToAssume: 'DelegateRecordUpdateRoleInThatAccount', 56 | * targetHostedZoneId: 'ZXXXXXXXXX', 57 | * zoneName: 'subdomain.mydomain.com', 58 | * }; 59 | * 60 | * new CrossAccountDNSDelegator(this, 'CrossAccountDNSDelegatorStack', crossAccountDNSDelegatorProps); 61 | */ 62 | export class CrossAccountDNSDelegator extends Construct { 63 | readonly hostedZone: route53.HostedZone; 64 | constructor(scope: Construct, id: string, props: ICrossAccountDNSDelegatorProps) { 65 | super(scope, id); 66 | 67 | const { 68 | targetAccount, 69 | targetRoleToAssume, 70 | targetHostedZoneId, 71 | zoneName, 72 | } = props; 73 | 74 | const hostedZone = new route53.HostedZone(this, 'HostedZone', { 75 | zoneName: zoneName, 76 | }); 77 | 78 | this.hostedZone = hostedZone; 79 | 80 | const delegatedNameServers: string[] = hostedZone.hostedZoneNameServers!; 81 | 82 | const currentAccountId = core.Stack.of(this).account; 83 | new CrossAccountZoneDelegationRecord(this, 'CrossAccountZoneDelegationRecord', { 84 | targetAccount: targetAccount, 85 | targetRoleToAssume: targetRoleToAssume, 86 | targetHostedZoneId: targetHostedZoneId, 87 | recordName: zoneName, 88 | toDelegateNameServers: delegatedNameServers, 89 | currentAccountId: currentAccountId, 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/dns/cross-account-zone-delegation-record-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import { Duration } from 'aws-cdk-lib'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 21 | import * as cr from 'aws-cdk-lib/custom-resources'; 22 | import { Construct } from 'constructs'; 23 | 24 | /** 25 | * A Custom Resource provider capable of creating a NS record with zone delegation 26 | * to the given name servers 27 | * 28 | * Note that there is no API to check the status of record creation. 29 | * Thus, we do not implement the `IsComplete` handler here. 30 | * The newly created record will be temporarily pending (a few seconds). 31 | */ 32 | export class CrossAccountZoneDelegationRecordProvider extends Construct { 33 | /** 34 | * The custom resource provider. 35 | */ 36 | public readonly provider: cr.Provider; 37 | 38 | /** 39 | * The onEvent handler 40 | */ 41 | public readonly onEventHandler: lambda.Function; 42 | 43 | constructor(scope: Construct, id: string, roleArnToAssume?: string) { 44 | super(scope, id); 45 | 46 | const code = lambda.Code.fromAsset(path.join(__dirname, 'delegation-record-handler')); 47 | 48 | // Handle CREATE/UPDATE/DELETE cross account 49 | this.onEventHandler = new lambda.Function(this, 'OnEventHandler', { 50 | code, 51 | runtime: lambda.Runtime.NODEJS_14_X, 52 | handler: 'index.onEventHandler', 53 | timeout: Duration.minutes(5), 54 | description: 'Cross-account zone delegation record OnEventHandler', 55 | }); 56 | 57 | // Allow to assume DNS account's updater role 58 | // roleArn, if not provided will be resolved in the lambda itself but still need to be allowed to assume it. 59 | this.onEventHandler.addToRolePolicy( 60 | new iam.PolicyStatement({ 61 | actions: ['sts:AssumeRole'], 62 | resources: [roleArnToAssume ? roleArnToAssume : '*'], 63 | }), 64 | ); 65 | 66 | //Allow to retrieve dynamically the zoneId and the target accountId 67 | this.onEventHandler.addToRolePolicy( 68 | new iam.PolicyStatement({ 69 | actions: ['route53:listHostedZonesByName', 'organizations:ListAccounts'], 70 | resources: ['*'], 71 | }), 72 | ); 73 | 74 | this.provider = new cr.Provider(this, 'CrossAccountZoneDelegationRecordProvider', { 75 | onEventHandler: this.onEventHandler, 76 | }); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/dns/cross-account-zone-delegation-record.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as core from 'aws-cdk-lib'; 3 | import { Construct } from 'constructs'; 4 | import { CrossAccountZoneDelegationRecordProvider } from './cross-account-zone-delegation-record-provider'; 5 | 6 | export interface CrossAccountZoneDelegationRecordProps { 7 | targetAccount?: string; 8 | targetRoleToAssume?: string; 9 | targetHostedZoneId?: string; 10 | recordName: string; 11 | toDelegateNameServers: string[]; 12 | currentAccountId: string; 13 | } 14 | 15 | /** 16 | * Create a NS zone delegation record in the target account 17 | */ 18 | export class CrossAccountZoneDelegationRecord extends Construct { 19 | 20 | constructor(scope: Construct, id: string, props: CrossAccountZoneDelegationRecordProps) { 21 | super(scope, id); 22 | 23 | const { targetAccount, targetRoleToAssume } = props; 24 | const roleArnToAssume = targetAccount && targetRoleToAssume ? 25 | `arn:aws:iam::${targetAccount}:role/${targetRoleToAssume}` 26 | :undefined; 27 | 28 | const stack = core.Stack.of(this); 29 | const crossAccountZoneDelegationRecordProvider = new CrossAccountZoneDelegationRecordProvider( 30 | stack, 31 | 'CrossAccountZoneDelegationRecordProvider', 32 | roleArnToAssume, 33 | ); 34 | 35 | new core.CustomResource( 36 | this, 37 | `CrossAccountZoneDelegationRecord-${props.recordName}`, 38 | { 39 | serviceToken: crossAccountZoneDelegationRecordProvider.provider.serviceToken, 40 | resourceType: 'Custom::CrossAccountZoneDelegationRecord', 41 | properties: { 42 | ...props, 43 | }, 44 | }, 45 | ); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/dns/delegation-record-handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | import type { OnEventResponse } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 19 | 20 | import * as AWS from 'aws-sdk'; 21 | import { ResourceRecords } from 'aws-sdk/clients/route53'; 22 | import Route53 = require('aws-sdk/clients/route53'); 23 | import { APIVersions } from 'aws-sdk/lib/config'; 24 | import { ServiceConfigurationOptions } from 'aws-sdk/lib/service'; 25 | import { getDNSUpdateRoleNameFromServiceRecordName } from './utils'; 26 | 27 | const AWS_API_VERSION = '2013-04-01'; 28 | 29 | /** 30 | * Assume role and return Route53 client with credentials to that role 31 | * @param roleArn 32 | * @param roleSessionName 33 | */ 34 | async function assumeRoleAndGetRoute53Client( 35 | roleArn: string, 36 | roleSessionName: string, 37 | ) { 38 | const params: ServiceConfigurationOptions & 39 | Pick = { 40 | apiVersion: AWS_API_VERSION, 41 | }; 42 | 43 | const sts = new AWS.STS(); 44 | 45 | params.credentials = await sts 46 | .assumeRole({ 47 | RoleArn: roleArn, 48 | RoleSessionName: roleSessionName, 49 | }) 50 | .promise() 51 | .then(({ Credentials }) => ({ 52 | accessKeyId: Credentials!.AccessKeyId, 53 | secretAccessKey: Credentials!.SecretAccessKey, 54 | sessionToken: Credentials!.SessionToken, 55 | expiration: Credentials!.Expiration, 56 | })); 57 | 58 | return new AWS.Route53(params); 59 | } 60 | 61 | enum Route53ChangeAction { 62 | UPSERT = 'UPSERT', 63 | DELETE = 'DELETE', 64 | } 65 | 66 | interface ChangeRecordParams { 67 | route53: AWS.Route53; 68 | targetHostedZoneId: string; 69 | recordName: string; 70 | toDelegateNameServers: string[]; 71 | changeAction: Route53ChangeAction; 72 | } 73 | 74 | /** 75 | * Change the NS record of given parameter to the given name 76 | * `changeAction` field could be either UPSERT or DELETE 77 | * @param params: ChangeRecordParam 78 | */ 79 | async function changeRecord(params: ChangeRecordParams) { 80 | const { 81 | route53, 82 | targetHostedZoneId, 83 | recordName, 84 | toDelegateNameServers, 85 | changeAction, 86 | } = params; 87 | 88 | const resourceRecords: ResourceRecords = toDelegateNameServers.map( 89 | (nameServer: string) => { 90 | return { Value: nameServer }; 91 | }, 92 | ); 93 | 94 | try { 95 | const response = await route53 96 | .changeResourceRecordSets({ 97 | HostedZoneId: targetHostedZoneId, 98 | ChangeBatch: { 99 | Changes: [ 100 | { 101 | Action: changeAction, 102 | ResourceRecordSet: { 103 | Name: recordName, 104 | Type: 'NS', 105 | TTL: 300, 106 | ResourceRecords: resourceRecords, 107 | }, 108 | }, 109 | ], 110 | }, 111 | }) 112 | .promise(); 113 | console.log('response = ', response.ChangeInfo.Id); 114 | } catch (e) { 115 | throw e; 116 | } 117 | } 118 | 119 | /** 120 | * Create/Update/Delete a zone delegation record cross-account, depending on event.RequestType 121 | * Edge cases: 122 | * 1. CREATE: If the resource has already existed it will update NS to the given name 123 | * 2. UPDATE: If the resource is missing (was manually deleted), it will fail. 124 | * 3. DELETE: If the resource is missing (was manually deleted), it will fail. 125 | * @param event An event with the following ResourceProperties: targetAccount, targetRoleToAssume, targetHostedZoneId, toDelegateNameServers (string[]), recordName 126 | * @returns Returns a PhysicalResourceId corresponding to record id 127 | */ 128 | export async function onEventHandler(event: any): Promise { 129 | console.log('Event: %j', event); 130 | 131 | const { 132 | targetAccount, 133 | targetRoleToAssume, 134 | targetHostedZoneId, 135 | toDelegateNameServers, 136 | recordName, 137 | currentAccountId, 138 | } = event.ResourceProperties; 139 | 140 | 141 | const roleArn = 142 | targetAccount && targetRoleToAssume 143 | ? `arn:aws:iam::${targetAccount}:role/${targetRoleToAssume}` 144 | : await resolveRoleArn(recordName, currentAccountId); 145 | 146 | const roleSessionName = event.LogicalResourceId.substr(0, 64); 147 | const route53 = await assumeRoleAndGetRoute53Client( 148 | roleArn, 149 | roleSessionName, 150 | ); 151 | 152 | const _targetHostedZoneId = targetHostedZoneId?targetHostedZoneId:await resolveParentHostedZoneId(route53, recordName); 153 | 154 | console.log('roleArn = ', roleArn); 155 | console.log('targetHostedZoneId = ', _targetHostedZoneId); 156 | console.log('toDelegateNameServers = ', toDelegateNameServers); 157 | console.log('recordName = ', recordName); 158 | 159 | const baseChangeRecordProps = { 160 | route53, 161 | targetHostedZoneId: _targetHostedZoneId, 162 | recordName, 163 | toDelegateNameServers, 164 | }; 165 | 166 | switch (event.RequestType) { 167 | case 'Create': 168 | await changeRecord({ 169 | ...baseChangeRecordProps, 170 | changeAction: Route53ChangeAction.UPSERT, 171 | }); 172 | break; 173 | case 'Update': 174 | await changeRecord({ 175 | ...baseChangeRecordProps, 176 | changeAction: Route53ChangeAction.DELETE, 177 | }); 178 | await changeRecord({ 179 | ...baseChangeRecordProps, 180 | changeAction: Route53ChangeAction.UPSERT, 181 | }); 182 | break; 183 | case 'Delete': 184 | // Delete an existing one 185 | await changeRecord({ 186 | ...baseChangeRecordProps, 187 | changeAction: Route53ChangeAction.DELETE, 188 | }); 189 | } 190 | 191 | let physicalResourceId = `cross-account-record-${targetAccount?targetAccount:roleArn.split(':')[4]}-${recordName}`; 192 | 193 | return { 194 | PhysicalResourceId: physicalResourceId, 195 | }; 196 | } 197 | 198 | /** 199 | * A function in charge of resolving the hosted zone Id from a sub domain (i.e. if 'app1.dev.yourdomain.com' is given, dev.yourdomain.com zone id will be returned) 200 | * @param recordName The full DNS record name which will be stripped to extract the parent dns zone name used to resolved the zone id 201 | * @returns ParentHostedZoneId 202 | */ 203 | async function resolveParentHostedZoneId(route53Client: Route53, recordName: string) { 204 | const listHostedZoneByNameResult = await route53Client.listHostedZonesByName({ 205 | DNSName: recordName.split('.').splice(1).join('.'), 206 | }).promise(); 207 | return listHostedZoneByNameResult.HostedZones[0].Id; 208 | } 209 | 210 | /** 211 | * A function used to resolve the role ARN capable of modifying a DNS sub zone in a remote account 212 | * @param recordName The full DNS record name which will be stripped to resolve the second part of the role name 213 | * @param currentAccountId the current account Id used to resolve the first part of the role name 214 | */ 215 | async function resolveRoleArn(recordName: string, currentAccountId: string) { 216 | try { 217 | const orgClient = new AWS.Organizations({ region: 'us-east-1' }); 218 | const listAccountsResults = await orgClient.listAccounts().promise(); 219 | let targetAccountId; 220 | let targetRoleToAssume; 221 | for (const account of listAccountsResults.Accounts 222 | ? listAccountsResults.Accounts 223 | : []) { 224 | 225 | // Indentify main account which is the one hosting DNS root domain 226 | if (account.JoinedMethod === 'INVITED') { 227 | targetAccountId = account.Id; 228 | } else if (account.Id == currentAccountId) { 229 | 230 | targetRoleToAssume = getDNSUpdateRoleNameFromServiceRecordName(recordName); 231 | } 232 | } 233 | const roleArn = `arn:aws:iam::${targetAccountId}:role/${targetRoleToAssume}`; 234 | return roleArn; 235 | } catch (error) { 236 | console.error(`Failed to resolveRoleArn due to ${error}`); 237 | throw error; 238 | } 239 | } -------------------------------------------------------------------------------- /src/dns/delegation-record-handler/utils.ts: -------------------------------------------------------------------------------- 1 | const DNS_UPDATE_ROLE_SUFFIX = '-dns-update'; 2 | export const getDNSUpdateRoleNameFromSubZoneName = (subZoneName: string) => { 3 | return `${subZoneName}${DNS_UPDATE_ROLE_SUFFIX}`.toLocaleLowerCase(); 4 | }; 5 | 6 | export const getSubdomainPrefix = (accountName: string | undefined, accountStageName: string | undefined) => { 7 | if (!accountName) { 8 | throw new Error('accountName needs to be provided. aborting.'); 9 | } 10 | let prefix: string = accountStageName ? accountStageName : accountName!; 11 | prefix = prefix.toLowerCase(); 12 | prefix = prefix.replace(' ', '-'); 13 | return prefix; 14 | }; 15 | 16 | 17 | export const getDNSUpdateRoleNameFromServiceRecordName = (serviceRecordName: string) => { 18 | // Keep only the root domain as it is used for the role name "landingpage.dev.ilovemylocalfarmer.dev".split('.').splice(1).join('.') => 'dev.ilovemylocalfarmer.dev' 19 | return getDNSUpdateRoleNameFromSubZoneName(serviceRecordName 20 | .split('.') 21 | .splice(1) 22 | .join('.')); 23 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aws-organizations-stack'; 2 | export * from './dns'; 3 | export * from './account'; 4 | export * from './dns/cross-account-dns-delegator'; 5 | export * from './validate-email'; 6 | export * from './secure-root-user'; -------------------------------------------------------------------------------- /src/organization-handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | import type { 19 | OnEventResponse, 20 | } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 21 | 22 | // eslint-disable-line import/no-extraneous-dependencies 23 | import { AWSError, Organizations } from 'aws-sdk'; 24 | 25 | /** 26 | * A function capable of creating an Organisation if not already created 27 | * @param event An event with no ResourceProperties 28 | * @returns Returns a PhysicalResourceId corresponding to the Organization id (no need to wait and check for completion) 29 | */ 30 | export async function onEventHandler( 31 | event: any, 32 | ): Promise { 33 | console.log('Event: %j', event); 34 | 35 | const awsOrganizationsClient = new Organizations({ region: 'us-east-1' }); 36 | 37 | switch (event.RequestType) { 38 | case 'Create': 39 | let result; 40 | let existingOrg = false; 41 | try { 42 | result = await awsOrganizationsClient 43 | .describeOrganization() 44 | .promise(); 45 | 46 | existingOrg = true; 47 | console.log('existing organization: %j', result); 48 | } catch (error) { 49 | if ((error as AWSError).code === 'AWSOrganizationsNotInUseException') { 50 | result = await awsOrganizationsClient 51 | .createOrganization() 52 | .promise(); 53 | 54 | console.log('created organization: %j', result); 55 | } else { 56 | throw error; 57 | } 58 | } 59 | return { PhysicalResourceId: result.Organization?.Id, Data: { OrganizationId: result.Organization?.Id, ExistingOrg: existingOrg } }; 60 | default: 61 | // we don't delete the org, just the custom resource 62 | return { PhysicalResourceId: event.PhysicalResourceId, Data: { OrganizationId: event.PhysicalResourceId } }; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/organization-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import { Duration } from 'aws-cdk-lib'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 21 | import { Provider } from 'aws-cdk-lib/custom-resources'; 22 | import { Construct } from 'constructs'; 23 | 24 | /** 25 | * A Custom Resource provider capable of creating AWS Organization or reusing an existing one 26 | */ 27 | export class OrganizationProvider extends Construct { 28 | /** 29 | * The custom resource provider. 30 | */ 31 | public readonly provider: Provider; 32 | 33 | constructor(scope: Construct, id: string) { 34 | super(scope, id); 35 | 36 | let createOrgHandler = new lambda.Function(this, 'OnEventHandler', { 37 | code: lambda.Code.fromAsset(path.join(__dirname, 'organization-handler')), 38 | runtime: lambda.Runtime.NODEJS_14_X, 39 | handler: 'index.onEventHandler', 40 | timeout: Duration.minutes(5), 41 | }); 42 | 43 | createOrgHandler.addToRolePolicy( 44 | new iam.PolicyStatement({ 45 | actions: [ 46 | 'organizations:CreateOrganization', 47 | 'organizations:DescribeOrganization', 48 | ], 49 | resources: ['*'], 50 | }), 51 | ); 52 | 53 | /* 54 | * the lambda needs to have the iam:CreateServiceLinkedRole permission so that the AWS Organizations service can create 55 | * Service Linked Role on its behalf 56 | */ 57 | createOrgHandler.addToRolePolicy( 58 | new iam.PolicyStatement({ 59 | actions: ['iam:CreateServiceLinkedRole'], 60 | resources: ['arn:aws:iam::*:role/*'], 61 | }), 62 | ); 63 | 64 | this.provider = new Provider(this, 'orgProvider', { 65 | onEventHandler: createOrgHandler, 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/organization-trail.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as core from 'aws-cdk-lib'; 18 | import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 19 | import { Bucket, BlockPublicAccess } from 'aws-cdk-lib/aws-s3'; 20 | import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources'; 21 | import { Construct } from 'constructs'; 22 | 23 | /** 24 | * The properties of an OrganizationTrail 25 | */ 26 | export interface IOrganizationTrailProps { 27 | /** 28 | * The Id of the organization which the trail works on 29 | */ 30 | OrganizationId: string; 31 | } 32 | 33 | /** 34 | * This represents an organization trail. An organization trail logs all events for all AWS accounts in that organization 35 | * and write them in a dedicated S3 bucket in the master account of the organization. To deploy this construct you should 36 | * the credential of the master account of your organization. It deploys a S3 bucket, enables cloudtrail.amazonaws.com to 37 | * access the organization API, creates an organization trail and 38 | * start logging. To learn about AWS Cloud Trail and organization trail, 39 | * check https://docs.aws.amazon.com/awscloudtrail/latest/userguide/creating-trail-organization.html 40 | */ 41 | export class OrganizationTrail extends Construct { 42 | 43 | constructor(scope: Construct, id: string, props: IOrganizationTrailProps) { 44 | super(scope, id); 45 | 46 | const orgTrailBucket = new Bucket(this, 'OrganizationTrailBucket', { blockPublicAccess: BlockPublicAccess.BLOCK_ALL }); 47 | 48 | orgTrailBucket.addToResourcePolicy(new PolicyStatement({ 49 | actions: ['s3:GetBucketAcl'], 50 | effect: Effect.ALLOW, 51 | principals: [new ServicePrincipal('cloudtrail.amazonaws.com')], 52 | resources: [orgTrailBucket.bucketArn], 53 | })); 54 | 55 | orgTrailBucket.addToResourcePolicy(new PolicyStatement({ 56 | actions: ['s3:PutObject'], 57 | effect: Effect.ALLOW, 58 | principals: [new ServicePrincipal('cloudtrail.amazonaws.com')], 59 | resources: [orgTrailBucket.bucketArn + '/AWSLogs/' + props.OrganizationId + '/*'], 60 | conditions: { 61 | StringEquals: 62 | { 63 | 's3:x-amz-acl': 'bucket-owner-full-control', 64 | }, 65 | }, 66 | })); 67 | 68 | orgTrailBucket.addToResourcePolicy(new PolicyStatement({ 69 | actions: ['s3:PutObject'], 70 | effect: Effect.ALLOW, 71 | principals: [new ServicePrincipal('cloudtrail.amazonaws.com')], 72 | resources: [orgTrailBucket.bucketArn + '/AWSLogs/' + core.Stack.of(this).account + '/*'], 73 | conditions: { 74 | StringEquals: 75 | { 76 | 's3:x-amz-acl': 'bucket-owner-full-control', 77 | }, 78 | }, 79 | })); 80 | 81 | const enableAWSServiceAccess = new AwsCustomResource(this, 82 | 'EnableAWSServiceAccess', 83 | { 84 | onCreate: { 85 | service: 'Organizations', 86 | action: 'enableAWSServiceAccess', //call enableAWSServiceAcces of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Organizations.html#enableAWSServiceAccess-property 87 | physicalResourceId: PhysicalResourceId.of('EnableAWSServiceAccess'), 88 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 89 | parameters: 90 | { 91 | ServicePrincipal: 'cloudtrail.amazonaws.com', 92 | }, 93 | }, 94 | onDelete: { 95 | service: 'Organizations', 96 | action: 'disableAWSServiceAccess', //call disableAWSServiceAcces of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Organizations.html#disableAWSServiceAccess-property 97 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 98 | parameters: 99 | { 100 | ServicePrincipal: 'cloudtrail.amazonaws.com', 101 | }, 102 | }, 103 | installLatestAwsSdk: false, 104 | policy: AwsCustomResourcePolicy.fromSdkCalls( 105 | { 106 | resources: AwsCustomResourcePolicy.ANY_RESOURCE, 107 | }, 108 | ), 109 | }, 110 | ); 111 | 112 | const organizationTrailName = 'OrganizationTrail'; 113 | 114 | let organizationTrailCreate = new AwsCustomResource(this, 115 | 'OrganizationTrailCreate', 116 | { 117 | onCreate: { 118 | service: 'CloudTrail', 119 | action: 'createTrail', //call createTrail of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudTrail.html#createTrail-property 120 | physicalResourceId: PhysicalResourceId.of('OrganizationTrailCreate'), 121 | parameters: 122 | { 123 | IsMultiRegionTrail: true, 124 | IsOrganizationTrail: true, 125 | Name: organizationTrailName, 126 | S3BucketName: orgTrailBucket.bucketName, 127 | }, 128 | }, 129 | onDelete: { 130 | service: 'CloudTrail', 131 | action: 'deleteTrail', //call deleteTrail of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudTrail.html#deleteTrail-property 132 | parameters: 133 | { 134 | Name: organizationTrailName, 135 | }, 136 | 137 | }, 138 | installLatestAwsSdk: false, 139 | policy: AwsCustomResourcePolicy.fromSdkCalls( 140 | { 141 | resources: AwsCustomResourcePolicy.ANY_RESOURCE, 142 | }, 143 | ), 144 | }, 145 | ); 146 | organizationTrailCreate.node.addDependency(enableAWSServiceAccess); 147 | // need to add an explicit dependency on the bucket policy to avoid the creation of the trail before the policy is set up 148 | if (orgTrailBucket.policy) { 149 | organizationTrailCreate.node.addDependency(orgTrailBucket.policy); 150 | } 151 | 152 | organizationTrailCreate.grantPrincipal.addToPrincipalPolicy(PolicyStatement.fromJson( 153 | { 154 | Effect: 'Allow', 155 | Action: [ 156 | 'iam:GetRole', 157 | 'organizations:EnableAWSServiceAccess', 158 | 'organizations:ListAccounts', 159 | 'iam:CreateServiceLinkedRole', 160 | 'organizations:DisableAWSServiceAccess', 161 | 'organizations:DescribeOrganization', 162 | 'organizations:ListAWSServiceAccessForOrganization', 163 | ], 164 | Resource: '*', 165 | }, 166 | )); 167 | 168 | const startLogging = new AwsCustomResource(this, 169 | 'OrganizationTrailStartLogging', 170 | { 171 | onCreate: { 172 | service: 'CloudTrail', 173 | action: 'startLogging', //call startLogging of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudTrail.html#startLogging-property 174 | physicalResourceId: PhysicalResourceId.of('OrganizationTrailStartLogging'), 175 | parameters: 176 | { 177 | Name: organizationTrailName, 178 | }, 179 | }, 180 | onDelete: { 181 | service: 'CloudTrail', 182 | action: 'stopLogging', //call stopLogging of the Javascript SDK https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudTrail.html#stopLogging-property 183 | physicalResourceId: PhysicalResourceId.of('OrganizationTrailStartLogging'), 184 | parameters: 185 | { 186 | Name: organizationTrailName, 187 | }, 188 | }, 189 | installLatestAwsSdk: false, 190 | policy: AwsCustomResourcePolicy.fromSdkCalls( 191 | { 192 | resources: AwsCustomResourcePolicy.ANY_RESOURCE, 193 | }, 194 | ), 195 | }, 196 | ); 197 | startLogging.node.addDependency(organizationTrailCreate); 198 | } 199 | } -------------------------------------------------------------------------------- /src/organization.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { CustomResource } from 'aws-cdk-lib'; 18 | import * as cr from 'aws-cdk-lib/custom-resources'; 19 | import { Construct } from 'constructs'; 20 | import { OrganizationProvider } from './organization-provider'; 21 | 22 | /** 23 | * This represents an Organization 24 | */ 25 | export class Organization extends Construct { 26 | /** 27 | * The Id of the Organization 28 | */ 29 | readonly id: string; 30 | 31 | /** 32 | * The Id of the root Organizational Unit of the Organization 33 | */ 34 | readonly rootId: string; 35 | 36 | constructor(scope: Construct, id: string) { 37 | super(scope, id); 38 | 39 | let orgProvider = new OrganizationProvider(this, 'orgProvider'); 40 | 41 | let org = new CustomResource(this, 'orgCustomResource', { 42 | serviceToken: orgProvider.provider.serviceToken, 43 | resourceType: 'Custom::OrganizationCreation', 44 | }); 45 | 46 | this.id = org.getAtt('OrganizationId').toString(); 47 | 48 | let root = new cr.AwsCustomResource(this, 'RootCustomResource', { 49 | onCreate: { 50 | service: 'Organizations', 51 | action: 'listRoots', 52 | physicalResourceId: cr.PhysicalResourceId.fromResponse('Roots.0.Id'), 53 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 54 | }, 55 | onUpdate: { 56 | service: 'Organizations', 57 | action: 'listRoots', 58 | physicalResourceId: cr.PhysicalResourceId.fromResponse('Roots.0.Id'), 59 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 60 | }, 61 | onDelete: { 62 | service: 'Organizations', 63 | action: 'listRoots', 64 | physicalResourceId: cr.PhysicalResourceId.fromResponse('Roots.0.Id'), 65 | region: 'us-east-1', //AWS Organizations API are only available in us-east-1 for root actions 66 | }, 67 | installLatestAwsSdk: false, 68 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 69 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 70 | }), 71 | }); 72 | 73 | // Enabling SSM AWS Service access to be able to register delegated adminstrator 74 | const enableSSMAWSServiceAccess = this.enableAWSServiceAccess('ssm.amazonaws.com'); 75 | const enableMultiAccountsSetupAWSServiceAccess = this.enableAWSServiceAccess('config-multiaccountsetup.amazonaws.com'); 76 | 77 | enableMultiAccountsSetupAWSServiceAccess.node.addDependency(org); 78 | enableSSMAWSServiceAccess.node.addDependency(enableMultiAccountsSetupAWSServiceAccess); 79 | 80 | //adding an explicit dependency as CloudFormation won't infer that calling listRoots must be done only when Organization creation is finished 81 | //as there is no implicit dependency between the 2 custom resources 82 | root.node.addDependency(org); 83 | 84 | this.rootId = root.getResponseField('Roots.0.Id'); 85 | } 86 | 87 | private enableAWSServiceAccess(principal: string) { 88 | const resourceName = 89 | principal === 'ssm.amazonaws.com' 90 | ? 'EnableSSMAWSServiceAccess' 91 | : 'EnableMultiAccountsSetup'; 92 | 93 | return new cr.AwsCustomResource(this, resourceName, { 94 | onCreate: { 95 | service: 'Organizations', 96 | action: 'enableAWSServiceAccess', 97 | physicalResourceId: cr.PhysicalResourceId.of(resourceName), 98 | region: 'us-east-1', 99 | parameters: { 100 | ServicePrincipal: principal, 101 | }, 102 | }, 103 | onDelete: { 104 | service: 'Organizations', 105 | action: 'disableAWSServiceAccess', 106 | region: 'us-east-1', 107 | parameters: { 108 | ServicePrincipal: principal, 109 | }, 110 | }, 111 | installLatestAwsSdk: false, 112 | policy: cr.AwsCustomResourcePolicy.fromSdkCalls({ 113 | resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE, 114 | }), 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/organizational-unit-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import { Duration, NestedStack, Stack } from 'aws-cdk-lib'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 21 | import { Provider } from 'aws-cdk-lib/custom-resources'; 22 | import { Construct } from 'constructs'; 23 | 24 | /** 25 | * A Custom Resource provider capable of creating AWS Organizational Units (OUs) or reusing an existing one 26 | */ 27 | export class OrganizationalUnitProvider extends NestedStack { 28 | 29 | /** 30 | * Creates a stack-singleton resource provider nested stack. 31 | */ 32 | public static getOrCreate(scope: Construct) { 33 | const stack = Stack.of(scope); 34 | const uid = '@aws-cdk/aws-bootstrap-kit.OUProvider'; 35 | return stack.node.tryFindChild(uid) as OrganizationalUnitProvider || new OrganizationalUnitProvider(stack, uid); 36 | } 37 | 38 | /** 39 | * The custom resource provider. 40 | */ 41 | public readonly provider: Provider; 42 | 43 | private constructor(scope: Construct, id: string) { 44 | super(scope, id); 45 | 46 | let createOUHandler = new lambda.Function(this, 'OnEventHandler', { 47 | code: lambda.Code.fromAsset(path.join(__dirname, 'ou-handler')), 48 | runtime: lambda.Runtime.NODEJS_14_X, 49 | handler: 'index.onEventHandler', 50 | timeout: Duration.minutes(5), 51 | }); 52 | 53 | createOUHandler.addToRolePolicy( 54 | new iam.PolicyStatement({ 55 | actions: [ 56 | 'organizations:ListOrganizationalUnitsForParent', 57 | 'organizations:CreateOrganizationalUnit', 58 | 'organizations:UpdateOrganizationalUnit', 59 | ], 60 | resources: ['*'], 61 | }), 62 | ); 63 | 64 | this.provider = new Provider(this, 'OUProvider', { 65 | onEventHandler: createOUHandler, 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/organizational-unit.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { CustomResource } from 'aws-cdk-lib'; 18 | import { Construct } from 'constructs'; 19 | import { OrganizationalUnitProvider } from './organizational-unit-provider'; 20 | 21 | export interface OrganizationalUnitProps { 22 | Name: string; 23 | ParentId: string; 24 | } 25 | 26 | export class OrganizationalUnit extends Construct { 27 | 28 | readonly id: string; 29 | 30 | constructor(scope: Construct, id: string, props: OrganizationalUnitProps) { 31 | super(scope, id); 32 | 33 | const ouProvider = OrganizationalUnitProvider.getOrCreate(this); 34 | 35 | let ou = new CustomResource(this, `OU-${props.Name}`, { 36 | serviceToken: ouProvider.provider.serviceToken, 37 | resourceType: 'Custom::OUCreation', 38 | properties: { 39 | Name: props.Name, 40 | ParentId: props.ParentId, 41 | }, 42 | }); 43 | 44 | this.id = ou.getAtt('OrganizationalUnitId').toString(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/ou-handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /* eslint-disable no-console */ 18 | import type { 19 | OnEventResponse, 20 | } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 21 | 22 | // eslint-disable-line import/no-extraneous-dependencies 23 | import { Organizations } from 'aws-sdk'; 24 | import { OrganizationalUnit, ListOrganizationalUnitsForParentResponse, ListOrganizationalUnitsForParentRequest } from 'aws-sdk/clients/organizations'; 25 | 26 | /** 27 | * A function capable of creating an Organisational Unit if not already created 28 | * @param event An event with OU name and parentId in ResourceProperties 29 | * @returns Returns a PhysicalResourceId corresponding to the Organisational Unit id 30 | */ 31 | export async function onEventHandler( 32 | event: any, 33 | ): Promise { 34 | console.log('Event: %j', event); 35 | const awsOrganizationsClient = new Organizations({ region: 'us-east-1' }); 36 | 37 | switch (event.RequestType) { 38 | case 'Create': 39 | let OU = await searchOrganizationalUnit(awsOrganizationsClient, event.ResourceProperties.ParentId, event.ResourceProperties.Name); 40 | let existingOU = (OU != null); 41 | 42 | if (!existingOU) { 43 | console.log(`creating OU ${event.ResourceProperties.Name} under ${event.ResourceProperties.ParentId}`); 44 | let response = await awsOrganizationsClient.createOrganizationalUnit({ 45 | ParentId: event.ResourceProperties.ParentId, 46 | Name: event.ResourceProperties.Name, 47 | }).promise(); 48 | OU = response.OrganizationalUnit; 49 | } 50 | 51 | return { PhysicalResourceId: OU?.Id, Data: { OrganizationalUnitId: OU?.Id, ExistingOU: existingOU } }; 52 | 53 | case 'Update': 54 | console.log(`updating OU ${event.PhysicalResourceId} with ${event.ResourceProperties.Name} under ${event.ResourceProperties.ParentId}`); 55 | await awsOrganizationsClient.updateOrganizationalUnit({ 56 | OrganizationalUnitId: event.PhysicalResourceId, 57 | Name: event.ResourceProperties.Name, 58 | }).promise(); 59 | 60 | return { PhysicalResourceId: event.PhysicalResourceId, Data: { OrganizationalUnitId: event.PhysicalResourceId } }; 61 | default: 62 | // we cannot delete the OU as we don't delete accounts within the OU 63 | return { PhysicalResourceId: event.PhysicalResourceId, Data: { OrganizationalUnitId: event.PhysicalResourceId } }; 64 | } 65 | 66 | } 67 | 68 | async function searchOrganizationalUnit(awsOrganizationsClient:Organizations, parentId:string, name:string): Promise { 69 | let response:ListOrganizationalUnitsForParentResponse = {}; 70 | let params:ListOrganizationalUnitsForParentRequest = { 71 | ParentId: parentId, 72 | }; 73 | 74 | response = await awsOrganizationsClient.listOrganizationalUnitsForParent(params).promise(); 75 | let OU = response.OrganizationalUnits?.find(ou => ou.Name === name); 76 | if (OU) { return OU; } 77 | 78 | while (response.NextToken) { 79 | params.NextToken = response.NextToken; 80 | response = await awsOrganizationsClient.listOrganizationalUnitsForParent(params).promise(); 81 | let OU = response.OrganizationalUnits?.find(ou => ou.Name === name); 82 | if (OU) { return OU; } 83 | } 84 | 85 | return undefined; 86 | } 87 | -------------------------------------------------------------------------------- /src/secure-root-user.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | import * as core from 'aws-cdk-lib'; 17 | import * as config from 'aws-cdk-lib/aws-config'; 18 | import * as iam from 'aws-cdk-lib/aws-iam'; 19 | import * as sns from 'aws-cdk-lib/aws-sns'; 20 | import * as subs from 'aws-cdk-lib/aws-sns-subscriptions'; 21 | import { Construct } from 'constructs'; 22 | import { ConfigRecorder } from './aws-config-recorder'; 23 | 24 | 25 | export class SecureRootUser extends Construct { 26 | constructor(scope: Construct, id: string, notificationEmail: string) { 27 | super(scope, id); 28 | 29 | // Build notification topic 30 | const secureRootUserConfigTopic = new sns.Topic(this, 'SecureRootUserConfigTopic'); 31 | secureRootUserConfigTopic.addSubscription(new subs.EmailSubscription(notificationEmail)); 32 | 33 | 34 | // Enforce MFA 35 | const configRecorder = new ConfigRecorder(this, 'ConfigRecorder'); 36 | 37 | const enforceMFARule = new config.ManagedRule(this, 'EnableRootMfa', { 38 | identifier: 'ROOT_ACCOUNT_MFA_ENABLED', 39 | maximumExecutionFrequency: 40 | config.MaximumExecutionFrequency.TWENTY_FOUR_HOURS, 41 | }); 42 | 43 | // Enforce No root access key 44 | const enforceNoAccessKeyRule = new config.ManagedRule( 45 | this, 46 | 'NoRootAccessKey', 47 | { 48 | identifier: 'IAM_ROOT_ACCESS_KEY_CHECK', 49 | maximumExecutionFrequency: 50 | config.MaximumExecutionFrequency.TWENTY_FOUR_HOURS, 51 | }, 52 | ); 53 | 54 | // Create role used for auto remediation 55 | const autoRemediationRole = new iam.Role(this, 'AutoRemediationRole', { 56 | assumedBy: new iam.CompositePrincipal( 57 | new iam.ServicePrincipal('events.amazonaws.com'), 58 | new iam.ServicePrincipal('ssm.amazonaws.com'), 59 | ), 60 | }); 61 | 62 | // See: https://github.com/aws/aws-cdk/issues/16188 63 | const ssmaAsgRoleAsCfn = autoRemediationRole.node.defaultChild as iam.CfnRole; 64 | ssmaAsgRoleAsCfn.addOverride('Properties.AssumeRolePolicyDocument.Statement.0.Principal.Service', ['events.amazonaws.com', 'ssm.amazonaws.com']); 65 | 66 | enforceMFARule.node.addDependency(configRecorder); 67 | enforceNoAccessKeyRule.node.addDependency(configRecorder); 68 | 69 | secureRootUserConfigTopic.grantPublish(autoRemediationRole); 70 | 71 | // Create remediations by notifying owner 72 | 73 | const mfaRemediationInstructionMessage = `Your main account (${core.Stack.of(this).account}) root user still not have MFA activated.\n\t1. Go to https://signin.aws.amazon.com/console and sign in using your root account\n\t2. Go to https://console.aws.amazon.com/iam/home#/security_credentials\n\t3. Activate MFA`; 74 | this.addNotCompliancyNotificationMechanism(enforceMFARule, autoRemediationRole, secureRootUserConfigTopic, mfaRemediationInstructionMessage); 75 | 76 | const accessKeyRemediationInstructionMessage = `Your main account (${core.Stack.of(this).account}) root user have static access keys.\n\t1. Go to https://signin.aws.amazon.com/console and sign in using your root account\n\t2. Go to https://console.aws.amazon.com/iam/home#/security_credentials\n\t3. Delete your Access keys`; 77 | this.addNotCompliancyNotificationMechanism(enforceNoAccessKeyRule, autoRemediationRole, secureRootUserConfigTopic, accessKeyRemediationInstructionMessage); 78 | } 79 | 80 | 81 | private addNotCompliancyNotificationMechanism(enforceMFARule: config.ManagedRule, autoRemediationRole: iam.Role, secureRootUserConfigTopic: sns.Topic, message: string) { 82 | new config.CfnRemediationConfiguration(this, `Notification-${enforceMFARule.node.id}`, { 83 | configRuleName: enforceMFARule.configRuleName, 84 | targetId: 'AWS-PublishSNSNotification', 85 | targetType: 'SSM_DOCUMENT', 86 | targetVersion: '1', 87 | automatic: true, 88 | maximumAutomaticAttempts: 1, 89 | retryAttemptSeconds: 60, 90 | parameters: { 91 | AutomationAssumeRole: { 92 | StaticValue: { 93 | Values: [ 94 | autoRemediationRole.roleArn, 95 | ], 96 | }, 97 | }, 98 | TopicArn: { 99 | StaticValue: { 100 | Values: [ 101 | secureRootUserConfigTopic.topicArn, 102 | ], 103 | }, 104 | }, 105 | Message: { 106 | StaticValue: { 107 | Values: [ 108 | // WARNING: Limited to 256 char 109 | message, 110 | ], 111 | }, 112 | }, 113 | }, 114 | }); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/validate-email-handler/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import type { 18 | IsCompleteRequest, 19 | IsCompleteResponse, 20 | OnEventResponse, 21 | } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 22 | import { SES, config } from 'aws-sdk'; 23 | 24 | config.update({ region: 'us-east-1' }); 25 | 26 | /** 27 | * A function that send a verification email 28 | * @param event An event with the following ResourceProperties: email (coresponding to the root email) 29 | * @returns Returns a PhysicalResourceId 30 | */ 31 | export async function onEventHandler( 32 | event: any, 33 | ): Promise { 34 | console.log('Event: %j', event); 35 | 36 | if (event.RequestType === 'Create') { 37 | const ses = new SES(); 38 | await ses 39 | .verifyEmailIdentity({ EmailAddress: event.ResourceProperties.email }) 40 | .promise(); 41 | 42 | return { PhysicalResourceId: 'validateEmail' }; 43 | } 44 | 45 | if (event.RequestType === 'Delete') { 46 | return { PhysicalResourceId: event.PhysicalResourceId }; 47 | } 48 | } 49 | 50 | /** 51 | * A function that checks email has been verified 52 | * @param event An event with the following ResourceProperties: email (coresponding to the root email) 53 | * @returns A payload containing the IsComplete Flag requested by cdk Custom Resource to figure out if the email has been verified and if not retries later 54 | */ 55 | export async function isCompleteHandler( 56 | event: IsCompleteRequest, 57 | ): Promise { 58 | console.log('Event: %j', event); 59 | 60 | if (!event.PhysicalResourceId) { 61 | throw new Error('Missing PhysicalResourceId parameter.'); 62 | } 63 | 64 | const email = event.ResourceProperties.email; 65 | if (event.RequestType === 'Create') { 66 | const ses = new SES(); 67 | const response = await ses 68 | .getIdentityVerificationAttributes({ 69 | Identities: [email], 70 | }) 71 | .promise(); 72 | 73 | return { 74 | IsComplete: 75 | response.VerificationAttributes[email]?.VerificationStatus === 'Success', 76 | }; 77 | } 78 | if (event.RequestType === 'Delete') { 79 | return { IsComplete: true }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/validate-email-provider.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import * as path from 'path'; 18 | import { Duration, Stack, NestedStack, StackProps } from 'aws-cdk-lib'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 21 | import { Provider } from 'aws-cdk-lib/custom-resources'; 22 | import { Construct } from 'constructs'; 23 | 24 | 25 | export interface ValidateEmailProviderProps extends StackProps { 26 | timeout?: Duration; 27 | } 28 | /** 29 | * A Custom Resource provider capable of validating emails 30 | */ 31 | export default class ValidateEmailProvider extends NestedStack { 32 | /** 33 | * The custom resource provider. 34 | */ 35 | readonly provider: Provider; 36 | 37 | /** 38 | * Creates a stack-singleton resource provider nested stack. 39 | */ 40 | public static getOrCreate(scope: Construct, props: ValidateEmailProviderProps) { 41 | const stack = Stack.of(scope); 42 | const uid = 'aws-cdk-lib/aws-bootstrap-kit.ValidateEmailProvider'; 43 | return ( 44 | (stack.node.tryFindChild(uid) as ValidateEmailProvider) || 45 | new ValidateEmailProvider(stack, uid, props) 46 | ); 47 | } 48 | 49 | /** 50 | * Constructor 51 | * 52 | * @param scope The parent Construct instantiating this construct 53 | * @param id This instance name 54 | */ 55 | constructor(scope: Construct, id: string, props: ValidateEmailProviderProps) { 56 | super(scope, id); 57 | 58 | const code = lambda.Code.fromAsset( 59 | path.join(__dirname, 'validate-email-handler'), 60 | ); 61 | 62 | const onEventHandler = new lambda.Function(this, 'OnEventHandler', { 63 | code, 64 | runtime: lambda.Runtime.NODEJS_14_X, 65 | handler: 'index.onEventHandler', 66 | timeout: Duration.minutes(5), 67 | }); 68 | 69 | onEventHandler.addToRolePolicy( 70 | new iam.PolicyStatement({ 71 | actions: ['ses:verifyEmailIdentity'], 72 | resources: ['*'], 73 | }), 74 | ); 75 | 76 | const isCompleteHandler = new lambda.Function(this, 'IsCompleteHandler', { 77 | code, 78 | runtime: lambda.Runtime.NODEJS_14_X, 79 | handler: 'index.isCompleteHandler', 80 | timeout: props.timeout ? props.timeout : Duration.minutes(10), 81 | }); 82 | 83 | isCompleteHandler.addToRolePolicy( 84 | new iam.PolicyStatement({ 85 | actions: ['ses:getIdentityVerificationAttributes'], 86 | resources: ['*'], 87 | }), 88 | ); 89 | 90 | this.provider = new Provider(this, 'EmailValidationProvider', { 91 | onEventHandler: onEventHandler, 92 | isCompleteHandler: isCompleteHandler, 93 | queryInterval: Duration.seconds(10), 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/validate-email.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { CustomResource, Duration } from 'aws-cdk-lib'; 18 | import { Construct } from 'constructs'; 19 | import ValidateEmailProvider from './validate-email-provider'; 20 | 21 | /** 22 | * Properties of ValidateEmail 23 | */ 24 | export interface ValidateEmailProps { 25 | /** 26 | * Email address of the Root account 27 | */ 28 | readonly email: string; 29 | readonly timeout?: Duration; 30 | } 31 | 32 | /** 33 | * Email Validation 34 | */ 35 | export class ValidateEmail extends Construct { 36 | /** 37 | * Constructor 38 | * 39 | * @param scope The parent Construct instantiating this construct 40 | * @param id This instance name 41 | * @param accountProps ValidateEmail properties 42 | */ 43 | constructor(scope: Construct, id: string, props: ValidateEmailProps) { 44 | super(scope, id); 45 | 46 | const [prefix, domain] = props.email.split('@'); 47 | 48 | if (prefix?.includes('+')) { 49 | throw new Error('Root Email should be without + in it'); 50 | } 51 | 52 | const subAddressedEmail = prefix + '+aws@' + domain; 53 | 54 | const { provider } = ValidateEmailProvider.getOrCreate(this, { timeout: props.timeout }); 55 | 56 | new CustomResource(this, 'EmailValidateResource', { 57 | serviceToken: provider.serviceToken, 58 | resourceType: 'Custom::EmailValidation', 59 | properties: { 60 | email: subAddressedEmail, 61 | }, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/integ/organization-unit-provider.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @group integ/organization-unit-provider 3 | */ 4 | 5 | import { DeployStackResult } from 'aws-cdk/lib/api/deploy-stack'; 6 | import * as cdk from 'aws-cdk-lib'; 7 | import { Organizations, STS } from 'aws-sdk'; 8 | import { deployStack, destroyStack } from './utils'; 9 | import { OrganizationalUnit } from '../../src/organizational-unit'; 10 | 11 | jest.setTimeout(250000); 12 | 13 | // SETUP 14 | let integTestApp: cdk.App; 15 | let stack: cdk.Stack; 16 | let deployResult: DeployStackResult; 17 | const orgClient = new Organizations({ region: 'us-east-1' }); 18 | const sts = new STS({ region: 'us-east-1' }); 19 | 20 | beforeEach(() => { 21 | integTestApp = new cdk.App({ 22 | context: { 23 | '@aws-cdk/core:bootstrapQualifier': 'integtest', 24 | }, 25 | }); 26 | stack = new cdk.Stack(integTestApp, `OUManagementTest-${expect.getState().currentTestName}-Stack`); 27 | }); 28 | 29 | afterEach(async () => { 30 | if (deployResult) { 31 | await destroyStack(integTestApp, stack, true); 32 | await orgClient.deleteOrganizationalUnit({ OrganizationalUnitId: deployResult.outputs.OUId }).promise(); 33 | } 34 | }); 35 | 36 | test('create-ou', async () => { 37 | //GIVEN 38 | const ouName = 'OU'; 39 | const currentAccount = await sts.getCallerIdentity().promise().then(id => id.Account); 40 | const parentOrg = await orgClient.listParents({ ChildId: currentAccount! }).promise(); 41 | const ou = new OrganizationalUnit(stack, 'OU', { 42 | Name: ouName, 43 | ParentId: parentOrg.Parents![0].Id!, 44 | }); 45 | 46 | new cdk.CfnOutput(stack, 'OUId', { 47 | value: ou.id, 48 | }); 49 | 50 | 51 | // WHEN 52 | try { 53 | deployResult = await deployStack(integTestApp, stack, false); 54 | console.log('deployed'); 55 | } catch (error) { 56 | console.error('failed to deploy', error); 57 | fail(); 58 | } 59 | // THEN 60 | console.log('then'); 61 | const organizationalUnits = await orgClient 62 | .listOrganizationalUnitsForParent({ 63 | ParentId: parentOrg.Parents![0].Id!, 64 | }) 65 | .promise(); 66 | expect(organizationalUnits.OrganizationalUnits?.find((ou) => ou.Id === deployResult.outputs.OUId)?.Name).toBe(ouName); 67 | }, 300000); 68 | -------------------------------------------------------------------------------- /test/integ/utils.ts: -------------------------------------------------------------------------------- 1 | import * as cxapi from '@aws-cdk/cx-api'; 2 | import { SdkProvider } from 'aws-cdk/lib/api/aws-auth'; 3 | import { CloudFormationDeployments } from 'aws-cdk/lib/api/cloudformation-deployments'; 4 | import { DeployStackResult } from 'aws-cdk/lib/api/deploy-stack'; 5 | import { App, Stack } from 'aws-cdk-lib'; 6 | 7 | export const deployStack = async (app: App, stack: Stack, quiet?: boolean): Promise => { 8 | console.log('getStackArtifact'); 9 | const stackArtifact = getStackArtifact(app, stack); 10 | 11 | console.log('createCloudFormationDeployments'); 12 | const cloudFormation = await createCloudFormationDeployments(); 13 | 14 | console.log('deployStack'); 15 | return cloudFormation.deployStack({ 16 | stack: stackArtifact, 17 | quiet: quiet ? false : false, 18 | }); 19 | }; 20 | 21 | export const destroyStack = async (app: App, stack: Stack, quiet?: boolean, retryCount?: number): Promise => { 22 | const stackArtifact = getStackArtifact(app, stack); 23 | 24 | const cloudFormation = await createCloudFormationDeployments(); 25 | 26 | retryCount = retryCount || 1; 27 | while (retryCount >= 0) { 28 | try { 29 | await cloudFormation.destroyStack({ 30 | stack: stackArtifact, 31 | quiet: quiet ? quiet : true, 32 | }); 33 | } catch (e) { 34 | console.error('Fail to delete stack retrying'); 35 | if (retryCount == 0) { 36 | throw e; 37 | } 38 | } 39 | retryCount--; 40 | } 41 | }; 42 | 43 | export const getOutput = (deployment: DeployStackResult, stackName: string, outputName: string): string => { 44 | const matchingOuput = Object.fromEntries(Object.entries(deployment.outputs).filter(([key]) => key.startsWith(stackName+outputName))); 45 | if (Object.keys(matchingOuput).length === 0 ) { 46 | throw new Error(`No output matching ${outputName}!`); 47 | } 48 | return matchingOuput[Object.keys(matchingOuput)[0]]; 49 | }; 50 | 51 | const getStackArtifact = (app: App, stack: Stack): cxapi.CloudFormationStackArtifact => { 52 | const synthesized = app.synth(); 53 | 54 | // Reload the synthesized artifact for stack using the cxapi from dependencies 55 | const assembly = new cxapi.CloudAssembly(synthesized.directory); 56 | 57 | return cxapi.CloudFormationStackArtifact.fromManifest( 58 | assembly, 59 | stack.artifactId, 60 | synthesized.getStackArtifact(stack.artifactId).manifest, 61 | ) as cxapi.CloudFormationStackArtifact; 62 | }; 63 | 64 | const createCloudFormationDeployments = async (): Promise => { 65 | const sdkProvider = await SdkProvider.withAwsCliCompatibleDefaults({ 66 | profile: process.env.AWS_PROFILE, 67 | }); 68 | const cloudFormation = new CloudFormationDeployments({ sdkProvider }); 69 | 70 | return cloudFormation; 71 | }; -------------------------------------------------------------------------------- /test/unit/account-provider.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/account-provider 19 | */ 20 | 21 | import { OnEventRequest, IsCompleteRequest } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 22 | import * as AWS from 'aws-sdk-mock'; 23 | import * as sinon from 'sinon'; 24 | import { AccountType } from '../../src'; 25 | import { isCompleteHandler, onEventHandler } from '../../src/account-handler'; 26 | 27 | AWS.setSDK(require.resolve('aws-sdk')); 28 | 29 | const createEvent: OnEventRequest = { 30 | RequestType: 'Create', 31 | ServiceToken: 'fakeToken', 32 | ResponseURL: 'fakeUrl', 33 | StackId: 'fakeStackId', 34 | RequestId: 'fakeReqId', 35 | LogicalResourceId: 'fakeLogicalId', 36 | ResourceType: 'Custom::AccountCreation', 37 | ResourceProperties: { 38 | ServiceToken: 'fakeToken', 39 | Email: 'fakeAlias+fakeStage@amazon.com', 40 | AccountName: 'Workload-fakeStage', 41 | AccountType: AccountType.STAGE, 42 | StageName: 'stage1', 43 | StageOrder: '1', 44 | HostedServices: 'app1:app2:app3', 45 | }, 46 | }; 47 | 48 | 49 | const isCompleteCreateEvent: IsCompleteRequest = { 50 | RequestType: 'Create', 51 | ServiceToken: 'fakeToken', 52 | ResponseURL: 'fakeUrl', 53 | StackId: 'fakeStackId', 54 | RequestId: 'fakeReqId', 55 | LogicalResourceId: 'fakeLogicalId', 56 | ResourceType: 'Custom::AccountCreation', 57 | ResourceProperties: { 58 | ServiceToken: 'fakeToken', 59 | }, 60 | PhysicalResourceId: 'fakeRequestCreateAccountStatusId', 61 | }; 62 | 63 | const updateEvent: OnEventRequest = { 64 | ... createEvent, 65 | RequestType: 'Update', 66 | PhysicalResourceId: 'fakeRequestCreateAccountStatusId', 67 | }; 68 | 69 | 70 | afterEach(() => { 71 | AWS.restore(); 72 | }); 73 | 74 | test('on event creates account for Create requests', async () => { 75 | const createAccountRequestId = 'fakeReqId'; 76 | const createAccountMock = sinon.fake.resolves({ 77 | CreateAccountStatus: { Id: createAccountRequestId }, 78 | }); 79 | 80 | AWS.mock('Organizations', 'createAccount', createAccountMock); 81 | 82 | const data = await onEventHandler(createEvent); 83 | 84 | sinon.assert.calledWith(createAccountMock, { 85 | Email: 'fakeAlias+fakeStage@amazon.com', 86 | AccountName: 'Workload-fakeStage', 87 | }); 88 | 89 | expect(data).toEqual({ 90 | PhysicalResourceId: createAccountRequestId, 91 | }); 92 | }); 93 | 94 | test('on update event does not call createAccount for Update requests but forward properties to isCompleteHandler for tag updates', async () => { 95 | const createAccountMock = sinon.fake.resolves({}); 96 | 97 | AWS.mock('Organizations', 'createAccount', createAccountMock); 98 | 99 | const data = await onEventHandler(updateEvent); 100 | sinon.assert.notCalled(createAccountMock); 101 | expect(data).toEqual({ 102 | PhysicalResourceId: updateEvent.PhysicalResourceId, 103 | ResourceProperties: updateEvent.ResourceProperties, 104 | }); 105 | }); 106 | 107 | test('is complete for create throw without requestId', async () => { 108 | const describeCreateAccountStatusMock = sinon.fake.resolves({}); 109 | 110 | AWS.mock( 111 | 'Organizations', 112 | 'describeCreateAccountStatus', 113 | describeCreateAccountStatusMock, 114 | ); 115 | 116 | try { 117 | await isCompleteHandler({ 118 | RequestType: 'Create', 119 | ServiceToken: 'fakeToken', 120 | ResponseURL: 'fakeUrl', 121 | StackId: 'fakeStackId', 122 | RequestId: 'fakeReqId', 123 | LogicalResourceId: 'fakeLogicalId', 124 | ResourceType: 'Custom::AccountCreation', 125 | ResourceProperties: { 126 | ServiceToken: 'fakeToken', 127 | }, 128 | PhysicalResourceId: undefined, 129 | }); 130 | sinon.assert.fail(); 131 | } catch (error) { 132 | sinon.assert.notCalled(describeCreateAccountStatusMock); 133 | expect((error as Error).message).toEqual('Missing PhysicalResourceId parameter.'); 134 | } 135 | }); 136 | 137 | test('is complete for create returns false when account creation is in progress', async () => { 138 | const describeCreateAccountStatusMock = sinon.fake.resolves({ 139 | CreateAccountStatus: 'INPROGRESS', 140 | }); 141 | 142 | AWS.mock( 143 | 'Organizations', 144 | 'describeCreateAccountStatus', 145 | describeCreateAccountStatusMock, 146 | ); 147 | 148 | const data = await isCompleteHandler(isCompleteCreateEvent); 149 | 150 | expect(data.IsComplete).toBeFalsy; 151 | }); 152 | 153 | test('is complete for create returns true when account creation is complete', async () => { 154 | const describeCreateAccountStatusMock = sinon.fake.resolves({ 155 | CreateAccountStatus: { 156 | State: 'SUCCEEDED', 157 | AccountId: 'fakeAccountId', 158 | }, 159 | }); 160 | 161 | AWS.mock( 162 | 'Organizations', 163 | 'describeCreateAccountStatus', 164 | describeCreateAccountStatusMock, 165 | ); 166 | 167 | const data = await isCompleteHandler(isCompleteCreateEvent); 168 | 169 | expect(data.IsComplete).toBeTruthy; 170 | expect(data.Data?.AccountId).toEqual('fakeAccountId'); 171 | }); 172 | -------------------------------------------------------------------------------- /test/unit/account.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/account 19 | */ 20 | 21 | import { expect as expectCDK, haveResource, countResourcesLike } from '@aws-cdk/assert'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { Account, AccountType } from '../../src/account'; 24 | 25 | test("HappyCase no DNS don't set delegation", () => { 26 | const stack = new Stack(); 27 | new Account(stack, 'myAccount', { 28 | email: 'fakeEmail', 29 | name: 'fakeAccountName', 30 | type: AccountType.PLAYGROUND, 31 | parentOrganizationalUnitId: 'fakeOUId', 32 | }); 33 | 34 | expectCDK(stack).to( 35 | haveResource('Custom::AccountCreation', { 36 | Email: 'fakeEmail', 37 | AccountName: 'fakeAccountName', 38 | }), 39 | ); 40 | 41 | expectCDK(stack).to(haveResource('Custom::AWS', { 42 | Create: { 43 | 'Fn::Join': [ 44 | '', 45 | [ 46 | '{"service":"Organizations","action":"tagResource","region":"us-east-1","physicalResourceId":{"id":"tags-', 47 | { 48 | 'Fn::GetAtt': [ 49 | 'myAccountAccountfakeAccountNameA6CEFA53', 50 | 'AccountId', 51 | ], 52 | }, 53 | '"},"parameters":{"ResourceId":"', 54 | { 55 | 'Fn::GetAtt': [ 56 | 'myAccountAccountfakeAccountNameA6CEFA53', 57 | 'AccountId', 58 | ], 59 | }, 60 | '","Tags":[{"Key":"Email","Value":"fakeEmail"},{"Key":"AccountName","Value":"fakeAccountName"},{"Key":"AccountType","Value":"PLAYGROUND"}]}}', 61 | ], 62 | ], 63 | }, 64 | Update: { 65 | 'Fn::Join': [ 66 | '', 67 | [ 68 | '{"service":"Organizations","action":"tagResource","region":"us-east-1","physicalResourceId":{"id":"tags-', 69 | { 70 | 'Fn::GetAtt': [ 71 | 'myAccountAccountfakeAccountNameA6CEFA53', 72 | 'AccountId', 73 | ], 74 | }, 75 | '"},"parameters":{"ResourceId":"', 76 | { 77 | 'Fn::GetAtt': [ 78 | 'myAccountAccountfakeAccountNameA6CEFA53', 79 | 'AccountId', 80 | ], 81 | }, 82 | '","Tags":[{"Key":"Email","Value":"fakeEmail"},{"Key":"AccountName","Value":"fakeAccountName"},{"Key":"AccountType","Value":"PLAYGROUND"}]}}', 83 | ], 84 | ], 85 | }, 86 | Delete: { 87 | 'Fn::Join': [ 88 | '', 89 | [ 90 | '{"service":"Organizations","action":"untagResource","region":"us-east-1","parameters":{"ResourceId":"', 91 | { 92 | 'Fn::GetAtt': [ 93 | 'myAccountAccountfakeAccountNameA6CEFA53', 94 | 'AccountId', 95 | ], 96 | }, 97 | '","TagKeys":["Email","AccountName","AccountType"]}}', 98 | ], 99 | ], 100 | }, 101 | })); 102 | 103 | expectCDK(stack).to(countResourcesLike('Custom::AWS', 0, { 104 | Create: { 105 | 'Fn::Join': [ 106 | '', 107 | [ 108 | '{"service":"Organizations","action":"registerDelegatedAdministrator","physicalResourceId":{"id":"registerDelegatedAdministrator"},"region":"us-east-1","parameters":{"AccountId":"', 109 | { 110 | 'Fn::GetAtt': [ 111 | 'myAccountAccountfakeAccountNameA6CEFA53', 112 | 'AccountId', 113 | ], 114 | }, 115 | '","ServicePrincipal":"config-multiaccountsetup.amazonaws.com"}}', 116 | ], 117 | ], 118 | }, 119 | 120 | })); 121 | }); 122 | 123 | 124 | test('HappyCase with DNS create admin delegation', () => { 125 | const stack = new Stack(); 126 | stack.node.setContext('domain_name', 'example.com'); 127 | new Account(stack, 'myAccount', { 128 | email: 'fakeEmail', 129 | name: 'fakeAccountName', 130 | parentOrganizationalUnitId: 'fakeOUId', 131 | }); 132 | 133 | expectCDK(stack).to( 134 | haveResource('Custom::AccountCreation', { 135 | Email: 'fakeEmail', 136 | AccountName: 'fakeAccountName', 137 | }), 138 | ); 139 | 140 | expectCDK(stack).to(countResourcesLike('Custom::AWS', 1, { 141 | Create: { 142 | 'Fn::Join': [ 143 | '', 144 | [ 145 | '{"service":"Organizations","action":"registerDelegatedAdministrator","physicalResourceId":{"id":"registerDelegatedAdministrator"},"region":"us-east-1","parameters":{"AccountId":"', 146 | { 147 | 'Fn::GetAtt': [ 148 | 'myAccountAccountfakeAccountNameA6CEFA53', 149 | 'AccountId', 150 | ], 151 | }, 152 | '","ServicePrincipal":"config-multiaccountsetup.amazonaws.com"}}', 153 | ], 154 | ], 155 | }, 156 | 157 | })); 158 | }); -------------------------------------------------------------------------------- /test/unit/aws-organizations-stack.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/aws-organizations-stack 19 | */ 20 | 21 | import '@aws-cdk/assert/jest'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { version } from '../../package.json'; 24 | import { AccountType, AwsOrganizationsStack, AwsOrganizationsStackProps } from '../../src'; 25 | 26 | const awsOrganizationsStackProps: AwsOrganizationsStackProps = { 27 | email: 'test@test.com', 28 | nestedOU: [ 29 | { 30 | name: 'SDLC', 31 | accounts: [ 32 | { 33 | name: 'Account1', 34 | stageName: 'theStage', 35 | type: AccountType.STAGE, 36 | stageOrder: 1, 37 | }, 38 | { 39 | name: 'Account2', 40 | type: AccountType.STAGE, 41 | stageOrder: 2, 42 | }, 43 | ], 44 | }, 45 | { 46 | name: 'Prod', 47 | accounts: [ 48 | { 49 | name: 'Account3', 50 | type: AccountType.STAGE, 51 | stageOrder: 3, 52 | }, 53 | ], 54 | }, 55 | ], 56 | }; 57 | 58 | 59 | test('when I define nestedOUs it create the right OUs', () => { 60 | 61 | const stack = new Stack(); 62 | let awsOrganizationsStackProps: AwsOrganizationsStackProps; 63 | awsOrganizationsStackProps = { 64 | email: 'test@test.com', 65 | nestedOU: [ 66 | { 67 | name: 'SDLC', 68 | nestedOU: [{ 69 | name: 'App1', 70 | accounts: [ 71 | { 72 | name: 'Account1', 73 | type: AccountType.PLAYGROUND, 74 | hostedServices: ['app1'], 75 | }, 76 | ], 77 | }, 78 | { 79 | name: 'Sandbox', 80 | accounts: [ 81 | { 82 | name: 'App2', 83 | type: AccountType.PLAYGROUND, 84 | hostedServices: ['app2'], 85 | }, 86 | ], 87 | }], 88 | }, 89 | { 90 | name: 'Prod', 91 | accounts: [ 92 | { 93 | name: 'Account3', 94 | type: AccountType.STAGE, 95 | stageOrder: 3, 96 | stageName: 'stage3', 97 | hostedServices: ['app1', 'app2'], 98 | }, 99 | ], 100 | }, 101 | ], 102 | }; 103 | 104 | 105 | const awsOrganizationsStack = new AwsOrganizationsStack(stack, 'AWSOrganizationsStack', awsOrganizationsStackProps); 106 | 107 | expect(awsOrganizationsStack.templateOptions.description).toMatch(`(version:${version})`); 108 | 109 | expect(awsOrganizationsStack).toHaveResource('Custom::OrganizationCreation'); 110 | 111 | expect(awsOrganizationsStack).toCountResources('Custom::AccountCreation', 3); 112 | expect(awsOrganizationsStack).toCountResources('Custom::OUCreation', 4); 113 | }); 114 | 115 | 116 | test('when I define 1 OU with 3 accounts (1 existing) and 1 OU with 1 account then the stack should have 2 OU constructs and 3 account constructs', () => { 117 | 118 | const stack = new Stack(); 119 | let awsOrganizationsStackProps: AwsOrganizationsStackProps; 120 | awsOrganizationsStackProps = { 121 | email: 'test@test.com', 122 | nestedOU: [ 123 | { 124 | name: 'SDLC', 125 | accounts: [ 126 | { 127 | name: 'Account1', 128 | type: AccountType.PLAYGROUND, 129 | hostedServices: ['app1', 'app2'], 130 | }, 131 | { 132 | name: 'Account2', 133 | type: AccountType.STAGE, 134 | stageOrder: 1, 135 | stageName: 'stage1', 136 | hostedServices: ['app1', 'app2'], 137 | }, 138 | { 139 | name: 'Account4', 140 | type: AccountType.STAGE, 141 | stageOrder: 2, 142 | stageName: 'stage2', 143 | hostedServices: ['app1', 'app2'], 144 | existingAccountId: '123456789012', 145 | }, 146 | ], 147 | }, 148 | { 149 | name: 'Prod', 150 | accounts: [ 151 | { 152 | name: 'Account3', 153 | type: AccountType.STAGE, 154 | stageOrder: 3, 155 | stageName: 'stage3', 156 | hostedServices: ['app1', 'app2'], 157 | }, 158 | ], 159 | }, 160 | ], 161 | }; 162 | 163 | 164 | const awsOrganizationsStack = new AwsOrganizationsStack(stack, 'AWSOrganizationsStack', awsOrganizationsStackProps); 165 | 166 | expect(awsOrganizationsStack.templateOptions.description).toMatch(`(version:${version})`); 167 | 168 | expect(awsOrganizationsStack).toHaveResource('Custom::OrganizationCreation'); 169 | 170 | expect(awsOrganizationsStack).toHaveResource('Custom::OUCreation', { 171 | Name: 'SDLC', 172 | ParentId: { 173 | 'Fn::GetAtt': [ 174 | 'OrganizationRootCustomResource9416950B', 175 | 'Roots.0.Id', 176 | ], 177 | }, 178 | }); 179 | 180 | expect(awsOrganizationsStack).toHaveResource('Custom::AccountCreation', { 181 | Email: { 182 | 'Fn::Join': [ 183 | '', 184 | [ 185 | 'test+Account1-', 186 | { 187 | Ref: 'AWS::AccountId', 188 | }, 189 | '@test.com', 190 | ], 191 | ], 192 | }, 193 | AccountName: 'Account1', 194 | }); 195 | 196 | expect(awsOrganizationsStack).toHaveResource('Custom::AccountCreation', { 197 | Email: { 198 | 'Fn::Join': [ 199 | '', 200 | [ 201 | 'test+Account2-', 202 | { 203 | Ref: 'AWS::AccountId', 204 | }, 205 | '@test.com', 206 | ], 207 | ], 208 | }, 209 | AccountName: 'Account2', 210 | }); 211 | 212 | let descAccount = JSON.stringify({ 213 | service: 'Organizations', 214 | action: 'describeAccount', 215 | physicalResourceId: { 216 | responsePath: 'Account.Id', 217 | }, 218 | region: 'us-east-1', 219 | parameters: { 220 | AccountId: '123456789012', 221 | }, 222 | }); 223 | expect(awsOrganizationsStack).toHaveResource('Custom::AWS', { 224 | Create: descAccount, 225 | Update: descAccount, 226 | }); 227 | 228 | expect(awsOrganizationsStack).toHaveResource('Custom::OUCreation', { 229 | Name: 'Prod', 230 | ParentId: { 231 | 'Fn::GetAtt': [ 232 | 'OrganizationRootCustomResource9416950B', 233 | 'Roots.0.Id', 234 | ], 235 | }, 236 | }); 237 | 238 | expect(awsOrganizationsStack).toHaveResource('AWS::SSM::Parameter', { 239 | Type: 'String', 240 | Value: { 241 | 'Fn::Join': [ 242 | '', 243 | [ 244 | '{"email":"', 245 | { 246 | 'Fn::GetAtt': [ 247 | 'Account4ExistingAccountCustomResource669615B0', 248 | 'Account.Email', 249 | ], 250 | }, 251 | '","name":"', 252 | { 253 | 'Fn::GetAtt': [ 254 | 'Account4ExistingAccountCustomResource669615B0', 255 | 'Account.Name', 256 | ], 257 | }, 258 | '","parentOrganizationalUnitId":"', 259 | { 260 | 'Fn::GetAtt': [ 261 | 'SDLCOUOUSDLC916764DE', 262 | 'OrganizationalUnitId', 263 | ], 264 | }, 265 | '","type":"STAGE","stageName":"stage2","stageOrder":2,"hostedServices":["app1","app2"],"id":"123456789012"}', 266 | ], 267 | ], 268 | }, 269 | Description: { 270 | 'Fn::Join': [ 271 | '', 272 | ['Details of ', 273 | { 274 | 'Fn::GetAtt': [ 275 | 'Account4ExistingAccountCustomResource669615B0', 276 | 'Account.Name', 277 | ], 278 | }], 279 | ], 280 | }, 281 | Name: { 282 | 'Fn::Join': [ 283 | '', 284 | ['/accounts/', 285 | { 286 | 'Fn::GetAtt': [ 287 | 'Account4ExistingAccountCustomResource669615B0', 288 | 'Account.Name', 289 | ], 290 | }], 291 | ], 292 | }, 293 | }); 294 | 295 | expect(awsOrganizationsStack).toHaveResource('Custom::AccountCreation', { 296 | Email: { 297 | 'Fn::Join': [ 298 | '', 299 | [ 300 | 'test+Account3-', 301 | { 302 | Ref: 'AWS::AccountId', 303 | }, 304 | '@test.com', 305 | ], 306 | ], 307 | }, 308 | AccountName: 'Account3', 309 | }); 310 | 311 | }); 312 | 313 | test('should create root domain zone and stage based domain if rootHostedZoneDNSName is specified ', () => { 314 | const awsOrganizationsStack = new AwsOrganizationsStack( 315 | new Stack(), 316 | 'AWSOrganizationsStack', 317 | { 318 | ...awsOrganizationsStackProps, 319 | rootHostedZoneDNSName: 'yourdomain.com', 320 | }, 321 | ); 322 | 323 | expect(awsOrganizationsStack.rootDns?.rootHostedZone.zoneName).toEqual('yourdomain.com'); 324 | expect(awsOrganizationsStack.rootDns?.stagesHostedZones.length).toEqual(3); 325 | 326 | expect(awsOrganizationsStack).toHaveResource('AWS::Route53::HostedZone', { 327 | Name: 'yourdomain.com.', 328 | }); 329 | expect(awsOrganizationsStack).toCountResources('AWS::Route53::RecordSet', 3); 330 | expect(awsOrganizationsStack).toCountResources('AWS::Route53::HostedZone', 4); 331 | expect(awsOrganizationsStack).toHaveResource('AWS::Route53::RecordSet', { 332 | Name: 'thestage.yourdomain.com.', 333 | Type: 'NS', 334 | }); 335 | expect(awsOrganizationsStack).toHaveResource('AWS::Route53::RecordSet', { 336 | Name: 'account2.yourdomain.com.', 337 | Type: 'NS', 338 | }); 339 | expect(awsOrganizationsStack).toHaveResource('AWS::Route53::RecordSet', { 340 | Name: 'account3.yourdomain.com.', 341 | Type: 'NS', 342 | }); 343 | expect(awsOrganizationsStack).toHaveResource('AWS::Route53::HostedZone', { 344 | Name: 'account3.yourdomain.com.', 345 | }); 346 | 347 | expect(awsOrganizationsStack).toHaveResource('AWS::IAM::Role', { 348 | RoleName: 'account2.yourdomain.com-dns-update', 349 | }); 350 | }); 351 | 352 | test('should not create any zone if no domain is provided', () => { 353 | const awsOrganizationsStack = new AwsOrganizationsStack( 354 | new Stack(), 355 | 'AWSOrganizationsStack', 356 | { 357 | ...awsOrganizationsStackProps, 358 | }, 359 | ); 360 | 361 | expect(awsOrganizationsStack).toCountResources('AWS::Route53::HostedZone', 0); 362 | }); 363 | 364 | test('should not create root zone if existing root zone id is provided', () => { 365 | const awsOrganizationsStack = new AwsOrganizationsStack( 366 | new Stack(), 367 | 'AWSOrganizationsStack', 368 | { 369 | ...awsOrganizationsStackProps, 370 | rootHostedZoneDNSName: 'yourdomain.com', 371 | existingRootHostedZoneId: 'existing-root-zone-id', 372 | }, 373 | ); 374 | 375 | // Check root zone not created 376 | expect(awsOrganizationsStack).not.toHaveResource('AWS::Route53::HostedZone', { 377 | Name: 'yourdomain.com.', 378 | }); 379 | 380 | // Check child zones created 381 | expect(awsOrganizationsStack).toCountResources('AWS::Route53::HostedZone', 3); 382 | }); 383 | 384 | test('should have have email validation stack with forceEmailVerification set to true', () => { 385 | 386 | const awsOrganizationsStack = new AwsOrganizationsStack( 387 | new Stack(), 388 | 'AWSOrganizationsStack', 389 | { ...awsOrganizationsStackProps, forceEmailVerification: true }, 390 | ); 391 | 392 | expect(awsOrganizationsStack).toHaveResource('Custom::EmailValidation'); 393 | }); 394 | 395 | test('should not have have email validation stack with forceEmailVerification set to false', () => { 396 | 397 | const awsOrganizationsStack = new AwsOrganizationsStack( 398 | new Stack(), 399 | 'AWSOrganizationsStack', 400 | { ...awsOrganizationsStackProps, forceEmailVerification: false }, 401 | ); 402 | 403 | expect(awsOrganizationsStack).not.toHaveResource('Custom::EmailValidation'); 404 | }); 405 | 406 | test('should have have email validation stack by default without setting forceEmailVerification', () => { 407 | const awsOrganizationsStack = new AwsOrganizationsStack( 408 | new Stack(), 409 | 'AWSOrganizationsStack', 410 | awsOrganizationsStackProps, 411 | ); 412 | 413 | expect(awsOrganizationsStack).toHaveResource('Custom::EmailValidation'); 414 | }); -------------------------------------------------------------------------------- /test/unit/cross-account-dns-delegator-provider.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/cross-account-dns-delegator 19 | */ 20 | 21 | import { OnEventRequest } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 22 | import { examples as route53Examples } from 'aws-sdk/apis/route53-2013-04-01.examples.json'; 23 | import { examples as stsExamples } from 'aws-sdk/apis/sts-2011-06-15.examples.json'; 24 | import * as AWS from 'aws-sdk-mock'; 25 | import * as sinon from 'sinon'; 26 | import { onEventHandler } from '../../src/dns/delegation-record-handler/index'; 27 | 28 | AWS.setSDK(require.resolve('aws-sdk')); 29 | 30 | const assumedRole = stsExamples.AssumeRole[0].output; 31 | const changeResourceRecordSets = route53Examples.ChangeResourceRecordSets[0].output; 32 | const fakeRecordName = 'app.stage.domain.com'; 33 | const fakeCurrentAccountId = '987654321987'; 34 | const fakeNameServer = ['ns1.test.com', 'ns2.test.com']; 35 | const roleNameToAssume = 'stage.domain.com-dns-update'; 36 | const dnsAccountId = '123456789123'; 37 | const targetHostedZoneId = 'ABCDEFGHYZ'; 38 | 39 | const fakeListHostedZonesByNameResponse = { 40 | HostedZones: [ 41 | { 42 | Id: targetHostedZoneId, 43 | Name: `${fakeRecordName.split('.').splice(1).join('.')}.`, 44 | CallerReference: 'fakeCallerRef', 45 | Config: { 46 | PrivateZone: false, 47 | }, 48 | ResourceRecordSetCount: 3, 49 | }, 50 | { 51 | Id: 'ROOTHOSTEDZONEID', 52 | Name: `${fakeRecordName.split('.').splice(2).join('.')}.`, 53 | CallerReference: 'fakeCallerRef', 54 | Config: { 55 | PrivateZone: false, 56 | }, 57 | ResourceRecordSetCount: 7, 58 | }, 59 | ], 60 | IsTruncated: false, 61 | MaxItems: '100', 62 | }; 63 | 64 | const fakeListAccountsResponse = { 65 | Accounts: [ 66 | { 67 | Id: fakeCurrentAccountId, 68 | Arn: `arn:aws:organizations::111111111111:account/o-1111111/${fakeCurrentAccountId}`, 69 | Email: 'admin+Stage-111111111111@domain.com', 70 | Name: 'Stage', 71 | Status: 'ACTIVE', 72 | JoinedMethod: 'CREATED', 73 | JoinedTimestamp: '2020-11-08T16:12:36.557000+01:00', 74 | }, 75 | { 76 | Id: dnsAccountId, 77 | Arn: 'arn:aws:organizations::111111111111:account/o-1ftjq7eeqg/111111111111', 78 | Email: 'admin+main@domain.com', 79 | Name: 'Main', 80 | Status: 'ACTIVE', 81 | JoinedMethod: 'INVITED', 82 | JoinedTimestamp: '2020-11-08T16:09:23.737000+01:00', 83 | }, 84 | ], 85 | }; 86 | 87 | 88 | const createEvent: OnEventRequest = { 89 | RequestType: 'Create', 90 | ServiceToken: 'fakeToken', 91 | ResponseURL: 'fakeUrl', 92 | StackId: 'fakeStackId', 93 | RequestId: 'fakeReqId', 94 | LogicalResourceId: 'fakeLogicalId', 95 | ResourceType: 'Custom::CrossAccountZoneDelegationRecord', 96 | ResourceProperties: { 97 | ServiceToken: 'fakeToken', 98 | targetAccount: dnsAccountId, 99 | targetRoleToAssume: roleNameToAssume, 100 | targetHostedZoneId: targetHostedZoneId, 101 | toDelegateNameServers: fakeNameServer, 102 | recordName: fakeRecordName, 103 | currentAccountId: fakeCurrentAccountId, 104 | }, 105 | }; 106 | 107 | afterEach(() => { 108 | AWS.restore(); 109 | }); 110 | 111 | test('when everything provided the right role is assumed and the right resource is changed', async () => { 112 | const assumedRoleMock = sinon.fake.resolves(assumedRole); 113 | const changeResourceRecordSetsMock = sinon.fake.resolves(changeResourceRecordSets); 114 | 115 | AWS.mock('STS', 'assumeRole', assumedRoleMock); 116 | 117 | AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsMock); 118 | await onEventHandler(createEvent); 119 | 120 | sinon.assert.calledWith(assumedRoleMock, { 121 | RoleArn: `arn:aws:iam::${dnsAccountId}:role/${roleNameToAssume}`, 122 | RoleSessionName: 'fakeLogicalId', 123 | }); 124 | 125 | sinon.assert.calledWith(assumedRoleMock, { 126 | RoleArn: `arn:aws:iam::${dnsAccountId}:role/${roleNameToAssume}`, 127 | RoleSessionName: 'fakeLogicalId', 128 | }); 129 | 130 | sinon.assert.calledWith(changeResourceRecordSetsMock, 131 | { 132 | ChangeBatch: { 133 | Changes: [{ 134 | Action: 'UPSERT', 135 | ResourceRecordSet: { 136 | Name: fakeRecordName, 137 | ResourceRecords: [{ Value: fakeNameServer[0] }, { Value: fakeNameServer[1] }], 138 | TTL: 300, 139 | Type: 'NS', 140 | }, 141 | }], 142 | }, 143 | HostedZoneId: targetHostedZoneId, 144 | }); 145 | }); 146 | 147 | 148 | test('when nothing is provided the right role is assumed and the right resource is changed', async () => { 149 | const assumedRoleMock = sinon.fake.resolves(assumedRole); 150 | const changeResourceRecordSetsMock = sinon.fake.resolves(changeResourceRecordSets); 151 | const listHostedZonesByNameMock = sinon.fake.resolves(fakeListHostedZonesByNameResponse); 152 | const listAccountsMock = sinon.fake.resolves(fakeListAccountsResponse); 153 | 154 | AWS.mock('STS', 'assumeRole', assumedRoleMock); 155 | 156 | AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsMock); 157 | 158 | AWS.mock('Organizations', 'listAccounts', listAccountsMock); 159 | AWS.mock('Route53', 'listHostedZonesByName', listHostedZonesByNameMock); 160 | 161 | createEvent.ResourceProperties = { 162 | ServiceToken: 'fakeToken', 163 | recordName: fakeRecordName, 164 | toDelegateNameServers: fakeNameServer, 165 | currentAccountId: fakeCurrentAccountId, 166 | }; 167 | 168 | await onEventHandler(createEvent); 169 | 170 | sinon.assert.calledWith(listHostedZonesByNameMock, { 171 | DNSName: 'stage.domain.com', 172 | }); 173 | 174 | sinon.assert.calledWith(assumedRoleMock, { 175 | RoleArn: `arn:aws:iam::${dnsAccountId}:role/${roleNameToAssume}`, 176 | RoleSessionName: 'fakeLogicalId', 177 | }); 178 | 179 | sinon.assert.calledWith(changeResourceRecordSetsMock, 180 | { 181 | ChangeBatch: { 182 | Changes: [{ 183 | Action: 'UPSERT', 184 | ResourceRecordSet: { 185 | Name: fakeRecordName, 186 | ResourceRecords: [{ Value: fakeNameServer[0] }, { Value: fakeNameServer[1] }], 187 | TTL: 300, 188 | Type: 'NS', 189 | }, 190 | }], 191 | }, 192 | HostedZoneId: targetHostedZoneId, 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/unit/cross-account-dns-delegator.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/cross-account-dns-delegator 19 | */ 20 | 21 | import '@aws-cdk/assert/jest'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { CrossAccountDNSDelegator } from '../../src/dns/cross-account-dns-delegator'; 24 | 25 | test('subdomain created', () => { 26 | const stack = new Stack(); 27 | new CrossAccountDNSDelegator(stack, 'myAccount', { 28 | zoneName: 'appsubdomain.stagesubdomain.mydomain.com', 29 | }); 30 | 31 | expect(stack).toHaveResource('AWS::Route53::HostedZone', { 32 | Name: 'appsubdomain.stagesubdomain.mydomain.com.', 33 | }); 34 | 35 | expect(stack).toHaveResource('Custom::CrossAccountZoneDelegationRecord', { 36 | recordName: 'appsubdomain.stagesubdomain.mydomain.com', 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/organization-provider.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/organizational-provider 19 | */ 20 | 21 | import { OnEventRequest } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 22 | import * as AWS from 'aws-sdk-mock'; 23 | import * as sinon from 'sinon'; 24 | import { onEventHandler } from '../../src/organization-handler'; 25 | 26 | AWS.setSDK(require.resolve('aws-sdk')); 27 | 28 | class AWSError extends Error { 29 | code: string; 30 | 31 | constructor(m: string, c: string) { 32 | super(m); 33 | this.name = 'AWSError'; 34 | this.code = c; 35 | this.stack = (new Error()).stack; 36 | Object.setPrototypeOf(this, AWSError.prototype); 37 | } 38 | } 39 | 40 | const createEvent: OnEventRequest = { 41 | RequestType: 'Create', 42 | ServiceToken: 'fakeToken', 43 | ResponseURL: 'fakeUrl', 44 | StackId: 'fakeStackId', 45 | RequestId: 'fakeReqId', 46 | LogicalResourceId: 'fakeLogicalId', 47 | ResourceType: 'Custom::OrganizationCreation', 48 | ResourceProperties: { 49 | ServiceToken: 'fakeToken', 50 | }, 51 | }; 52 | 53 | afterEach(() => { 54 | AWS.restore(); 55 | }); 56 | 57 | test('on create event reuse Organization if already exists', async () => { 58 | const createOrgRequestId = 'fakeReqId'; 59 | 60 | const describeOrganizationMock = sinon.fake.resolves({ 61 | Organization: { Id: createOrgRequestId }, 62 | }); 63 | 64 | AWS.mock('Organizations', 'describeOrganization', describeOrganizationMock); 65 | 66 | const data = await onEventHandler(createEvent); 67 | 68 | expect(data).toEqual({ 69 | PhysicalResourceId: createOrgRequestId, 70 | Data: { 71 | OrganizationId: createOrgRequestId, 72 | ExistingOrg: true, 73 | }, 74 | }); 75 | }); 76 | 77 | test('on create event create Organization if not already exists', async () => { 78 | const error = new AWSError('Organization is not in use', 'AWSOrganizationsNotInUseException'); 79 | 80 | AWS.mock('Organizations', 'describeOrganization', sinon.fake.throws(error)); 81 | 82 | const createOrgRequestId = 'fakeReqId'; 83 | const createOrganizationMock = sinon.fake.resolves({ 84 | Organization: { Id: createOrgRequestId }, 85 | }); 86 | 87 | AWS.mock('Organizations', 'createOrganization', createOrganizationMock); 88 | 89 | const data = await onEventHandler(createEvent); 90 | 91 | sinon.assert.calledOnce(createOrganizationMock); 92 | 93 | expect(data).toEqual({ 94 | PhysicalResourceId: createOrgRequestId, 95 | Data: { 96 | OrganizationId: createOrgRequestId, 97 | ExistingOrg: false, 98 | }, 99 | }); 100 | }); -------------------------------------------------------------------------------- /test/unit/organizational-trail.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/organizational-trail 19 | */ 20 | 21 | import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { OrganizationTrail } from '../../src/organization-trail'; 24 | 25 | test('OrganizationTrail creation', () => { 26 | const stack = new Stack(); 27 | new OrganizationTrail(stack, 'OrganizationTrail', { OrganizationId: 'o-111111' }); 28 | 29 | expectCDK(stack).to( 30 | haveResource('AWS::S3::Bucket', { 31 | PublicAccessBlockConfiguration: { 32 | BlockPublicAcls: true, 33 | BlockPublicPolicy: true, 34 | IgnorePublicAcls: true, 35 | RestrictPublicBuckets: true, 36 | }, 37 | 38 | }), 39 | ); 40 | 41 | expectCDK(stack).to( 42 | haveResource('AWS::S3::BucketPolicy', { 43 | Bucket: { 44 | Ref: 'OrganizationTrailOrganizationTrailBucket31446F20', 45 | }, 46 | PolicyDocument: { 47 | Statement: [ 48 | { 49 | Action: 's3:GetBucketAcl', 50 | Effect: 'Allow', 51 | Principal: { 52 | Service: 'cloudtrail.amazonaws.com', 53 | }, 54 | Resource: { 55 | 'Fn::GetAtt': [ 56 | 'OrganizationTrailOrganizationTrailBucket31446F20', 57 | 'Arn', 58 | ], 59 | }, 60 | }, 61 | { 62 | Action: 's3:PutObject', 63 | Condition: { 64 | StringEquals: { 65 | 's3:x-amz-acl': 'bucket-owner-full-control', 66 | }, 67 | }, 68 | Effect: 'Allow', 69 | Principal: { 70 | Service: 'cloudtrail.amazonaws.com', 71 | }, 72 | Resource: { 73 | 'Fn::Join': [ 74 | '', 75 | [ 76 | { 77 | 'Fn::GetAtt': [ 78 | 'OrganizationTrailOrganizationTrailBucket31446F20', 79 | 'Arn', 80 | ], 81 | }, 82 | '/AWSLogs/o-111111/*', 83 | ], 84 | ], 85 | }, 86 | }, 87 | { 88 | Action: 's3:PutObject', 89 | Condition: { 90 | StringEquals: { 91 | 's3:x-amz-acl': 'bucket-owner-full-control', 92 | }, 93 | }, 94 | Effect: 'Allow', 95 | Principal: { 96 | Service: 'cloudtrail.amazonaws.com', 97 | }, 98 | Resource: { 99 | 'Fn::Join': [ 100 | '', 101 | [ 102 | { 103 | 'Fn::GetAtt': [ 104 | 'OrganizationTrailOrganizationTrailBucket31446F20', 105 | 'Arn', 106 | ], 107 | }, 108 | '/AWSLogs/', 109 | { 110 | Ref: 'AWS::AccountId', 111 | }, 112 | '/*', 113 | ], 114 | ], 115 | }, 116 | }, 117 | ], 118 | Version: '2012-10-17', 119 | }, 120 | 121 | }), 122 | ); 123 | 124 | expectCDK(stack).to( 125 | haveResource('Custom::AWS', { 126 | Create: JSON.stringify({ 127 | service: 'Organizations', 128 | action: 'enableAWSServiceAccess', 129 | physicalResourceId: { 130 | id: 'EnableAWSServiceAccess', 131 | }, 132 | region: 'us-east-1', 133 | parameters: { 134 | ServicePrincipal: 'cloudtrail.amazonaws.com', 135 | }, 136 | }), 137 | Delete: JSON.stringify({ 138 | service: 'Organizations', 139 | action: 'disableAWSServiceAccess', 140 | region: 'us-east-1', 141 | parameters: { 142 | ServicePrincipal: 'cloudtrail.amazonaws.com', 143 | }, 144 | }), 145 | }), 146 | ); 147 | 148 | expectCDK(stack).to( 149 | haveResource('Custom::AWS', { 150 | Create: { 151 | 'Fn::Join': [ 152 | '', 153 | [ 154 | '{"service":"CloudTrail","action":"createTrail","physicalResourceId":{"id":"OrganizationTrailCreate"},"parameters":{"IsMultiRegionTrail":true,"IsOrganizationTrail":true,"Name":"OrganizationTrail","S3BucketName":"', 155 | { 156 | Ref: 'OrganizationTrailOrganizationTrailBucket31446F20', 157 | }, 158 | '"}}', 159 | ], 160 | ], 161 | }, 162 | Delete: JSON.stringify({ 163 | service: 'CloudTrail', 164 | action: 'deleteTrail', 165 | parameters: { 166 | Name: 'OrganizationTrail', 167 | }, 168 | }), 169 | }), 170 | ); 171 | 172 | expectCDK(stack).to( 173 | haveResource('Custom::AWS', { 174 | Create: '{"service":"CloudTrail","action":"startLogging","physicalResourceId":{"id":"OrganizationTrailStartLogging"},"parameters":{"Name":"OrganizationTrail"}}', 175 | Delete: '{"service":"CloudTrail","action":"stopLogging","physicalResourceId":{"id":"OrganizationTrailStartLogging"},"parameters":{"Name":"OrganizationTrail"}}', 176 | }), 177 | ); 178 | }); 179 | -------------------------------------------------------------------------------- /test/unit/organizational-unit-provider.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/organizational-unit-provider 19 | */ 20 | 21 | import { OnEventRequest } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 22 | import * as AWS from 'aws-sdk-mock'; 23 | import * as sinon from 'sinon'; 24 | import { onEventHandler } from '../../src/ou-handler'; 25 | 26 | AWS.setSDK(require.resolve('aws-sdk')); 27 | 28 | const createEvent: OnEventRequest = { 29 | RequestType: 'Create', 30 | ServiceToken: 'fakeToken', 31 | ResponseURL: 'fakeUrl', 32 | StackId: 'fakeStackId', 33 | RequestId: 'fakeReqId', 34 | LogicalResourceId: 'fakeLogicalId', 35 | ResourceType: 'Custom::OrganizationalUnitCreation', 36 | ResourceProperties: { 37 | ServiceToken: 'fakeToken', 38 | ParentId: 'fakeParentId', 39 | Name: 'fakeOUName', 40 | }, 41 | }; 42 | 43 | const updateEvent: OnEventRequest = { 44 | RequestType: 'Update', 45 | ServiceToken: 'fakeToken', 46 | ResponseURL: 'fakeUrl', 47 | StackId: 'fakeStackId', 48 | RequestId: 'fakeReqId', 49 | LogicalResourceId: 'fakeLogicalId', 50 | PhysicalResourceId: 'ou-fakeOUId', 51 | ResourceType: 'Custom::OrganizationalUnitCreation', 52 | ResourceProperties: { 53 | ServiceToken: 'fakeToken', 54 | ParentId: 'fakeParentId', 55 | Name: 'fakeUdaptedOUName', 56 | }, 57 | }; 58 | 59 | afterEach(() => { 60 | AWS.restore(); 61 | }); 62 | 63 | test('on create event creates OU if does not exists', async () => { 64 | const OUId = 'ou-fakeOUId'; 65 | const listOUsMock = sinon.fake.resolves({ 66 | OrganizationalUnits: [ 67 | { 68 | Id: 'ou-abcd', 69 | Name: 'Abcd', 70 | }, 71 | { 72 | Id: 'ou-efgh', 73 | Name: 'Efgh', 74 | }, 75 | ], 76 | }); 77 | AWS.mock('Organizations', 'listOrganizationalUnitsForParent', listOUsMock); 78 | 79 | const createOUMock = sinon.fake.resolves({ 80 | OrganizationalUnit: { 81 | Id: OUId, 82 | Name: 'fakeOUName', 83 | }, 84 | }); 85 | AWS.mock('Organizations', 'createOrganizationalUnit', createOUMock); 86 | 87 | const data = await onEventHandler(createEvent); 88 | 89 | expect(data).toEqual({ 90 | PhysicalResourceId: OUId, 91 | Data: { 92 | OrganizationalUnitId: OUId, 93 | ExistingOU: false, 94 | }, 95 | }); 96 | }); 97 | 98 | test('on create event reuses OU if already exists', async () => { 99 | const OUId = 'ou-fakeOUId'; 100 | const listOUsMock = sinon.fake.resolves({ 101 | OrganizationalUnits: [ 102 | { 103 | Id: 'ou-abcd', 104 | Name: 'Abcd', 105 | }, 106 | { 107 | Id: OUId, 108 | Name: 'fakeOUName', 109 | }, 110 | ], 111 | }); 112 | AWS.mock('Organizations', 'listOrganizationalUnitsForParent', listOUsMock); 113 | 114 | const data = await onEventHandler(createEvent); 115 | 116 | expect(data).toEqual({ 117 | PhysicalResourceId: OUId, 118 | Data: { 119 | OrganizationalUnitId: OUId, 120 | ExistingOU: true, 121 | }, 122 | }); 123 | }); 124 | 125 | test('on update event, it reuses OU if already exists and change its name', async () => { 126 | const OUId = 'ou-fakeOUId'; 127 | const listOUsMock = sinon.fake.resolves({ 128 | OrganizationalUnits: [ 129 | { 130 | Id: 'ou-abcd', 131 | Name: 'Abcd', 132 | }, 133 | { 134 | Id: OUId, 135 | Name: 'fakeOUName', 136 | }, 137 | ], 138 | }); 139 | AWS.mock('Organizations', 'listOrganizationalUnitsForParent', listOUsMock); 140 | 141 | const updateOUMock = sinon.fake.resolves({ 142 | OrganizationalUnit: { 143 | Id: OUId, 144 | Name: 'fakeUpdatedOUName', 145 | }, 146 | }); 147 | AWS.mock('Organizations', 'updateOrganizationalUnit', updateOUMock); 148 | 149 | 150 | const data = await onEventHandler(updateEvent); 151 | 152 | expect(data).toEqual({ 153 | PhysicalResourceId: OUId, 154 | Data: { 155 | OrganizationalUnitId: OUId, 156 | }, 157 | }); 158 | }); 159 | 160 | test('on create event reuses OU if already exists (nextToken)', async () => { 161 | const OUId = 'ou-fakeOUId'; 162 | 163 | let stub = sinon.stub(); 164 | stub.onCall(0).resolves({ 165 | OrganizationalUnits: [ 166 | { 167 | Id: 'ou-abcd', 168 | Name: 'Abcd', 169 | }, 170 | { 171 | Id: 'ou-efgh', 172 | Name: 'Efgh', 173 | }, 174 | ], 175 | NextToken: 'abc123', 176 | }); 177 | stub.onCall(1).resolves({ 178 | OrganizationalUnits: [ 179 | { 180 | Id: 'ou-ijkl', 181 | Name: 'Ijkl', 182 | }, 183 | { 184 | Id: OUId, 185 | Name: 'fakeOUName', 186 | }, 187 | ], 188 | }); 189 | AWS.mock('Organizations', 'listOrganizationalUnitsForParent', stub); 190 | 191 | const data = await onEventHandler(createEvent); 192 | 193 | expect(data).toEqual({ 194 | PhysicalResourceId: OUId, 195 | Data: { 196 | OrganizationalUnitId: OUId, 197 | ExistingOU: true, 198 | }, 199 | }); 200 | 201 | sinon.assert.calledTwice(stub); 202 | }); 203 | -------------------------------------------------------------------------------- /test/unit/secure-root-user.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/secure-root-user 19 | */ 20 | 21 | import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { SecureRootUser } from '../../src/secure-root-user'; 24 | 25 | test('Get 2FA and Access key rules', () => { 26 | const stack = new Stack(); 27 | 28 | new SecureRootUser(stack, 'secureRootUser', 'test@amazon.com'); 29 | 30 | expectCDK(stack).to( 31 | haveResource('AWS::Config::ConfigRule', { 32 | Source: { 33 | Owner: 'AWS', 34 | SourceIdentifier: 'ROOT_ACCOUNT_MFA_ENABLED', 35 | }, 36 | }), 37 | ); 38 | expectCDK(stack).to( 39 | haveResource('AWS::Config::ConfigRule', { 40 | Source: { 41 | Owner: 'AWS', 42 | SourceIdentifier: 'IAM_ROOT_ACCESS_KEY_CHECK', 43 | }, 44 | }), 45 | ); 46 | 47 | expectCDK(stack).to( 48 | haveResource('AWS::SNS::Topic'), 49 | ); 50 | 51 | expectCDK(stack).to( 52 | haveResource('AWS::IAM::Role', { 53 | AssumeRolePolicyDocument: { 54 | Statement: [ 55 | { 56 | Action: 'sts:AssumeRole', 57 | Effect: 'Allow', 58 | Principal: { 59 | Service: [ 60 | 'events.amazonaws.com', 61 | 'ssm.amazonaws.com', 62 | ], 63 | }, 64 | }, 65 | { 66 | Action: 'sts:AssumeRole', 67 | Effect: 'Allow', 68 | Principal: { 69 | Service: 'ssm.amazonaws.com', 70 | }, 71 | }, 72 | ], 73 | Version: '2012-10-17', 74 | }, 75 | }), 76 | ); 77 | }); 78 | -------------------------------------------------------------------------------- /test/unit/validate-email.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @group unit/validate-email 19 | */ 20 | 21 | import '@aws-cdk/assert/jest'; 22 | import { Stack } from 'aws-cdk-lib'; 23 | import { 24 | OnEventRequest, 25 | IsCompleteRequest, 26 | } from 'aws-cdk-lib/custom-resources/lib/provider-framework/types'; 27 | import * as AWS from 'aws-sdk-mock'; 28 | import * as sinon from 'sinon'; 29 | import { ValidateEmail } from '../../src/validate-email'; 30 | import { 31 | isCompleteHandler, 32 | onEventHandler, 33 | } from '../../src/validate-email-handler'; 34 | 35 | test('Should throw Error if Email Prefix contains + ', () => { 36 | const validateEmailStack = () => 37 | new ValidateEmail(new Stack(), 'TestStack', { 38 | email: 'test+test@test.com', 39 | }); 40 | 41 | expect(validateEmailStack).toThrowError( 42 | 'Root Email should be without + in it', 43 | ); 44 | }); 45 | 46 | AWS.setSDK(require.resolve('aws-sdk')); 47 | 48 | const createEvent: OnEventRequest = { 49 | RequestType: 'Create', 50 | ServiceToken: 'fakeToken', 51 | ResponseURL: 'fakeUrl', 52 | StackId: 'fakeStackId', 53 | RequestId: 'fakeReqId', 54 | LogicalResourceId: 'fakeLogicalId', 55 | ResourceType: 'Custom::EmailValidation', 56 | ResourceProperties: { 57 | ServiceToken: 'fakeToken', 58 | email: 'test@test.com', 59 | }, 60 | }; 61 | 62 | const isCompleteCreateEvent: IsCompleteRequest = { 63 | RequestType: 'Create', 64 | ServiceToken: 'fakeToken', 65 | ResponseURL: 'fakeUrl', 66 | StackId: 'fakeStackId', 67 | RequestId: 'fakeReqId', 68 | LogicalResourceId: 'fakeLogicalId', 69 | ResourceType: 'Custom::EmailValidation', 70 | ResourceProperties: { 71 | ServiceToken: 'fakeToken', 72 | Email: 'test@test.com', 73 | }, 74 | PhysicalResourceId: 'fakeRequestCreateAccountStatusId', 75 | }; 76 | 77 | const deleteEvent = { ...createEvent, RequestType: 'Delete', PhysicalResourceId: 'validateEmail' }; 78 | const isCompleteDeleteEvent: IsCompleteRequest = { ...isCompleteCreateEvent, RequestType: 'Delete' }; 79 | 80 | afterEach(() => { 81 | AWS.restore(); 82 | }); 83 | 84 | test('on event create calls ses verifyEmailIdentity', async () => { 85 | const verifyEmailIdentityMock = sinon.fake.resolves(true); 86 | 87 | AWS.mock('SES', 'verifyEmailIdentity', verifyEmailIdentityMock); 88 | 89 | const data = await onEventHandler(createEvent); 90 | 91 | sinon.assert.calledWith(verifyEmailIdentityMock, { 92 | EmailAddress: 'test@test.com', 93 | }); 94 | 95 | expect(data).toEqual({ 96 | PhysicalResourceId: 'validateEmail', 97 | }); 98 | }); 99 | 100 | test('on completion event calls ses getIdentityVerificationAttributes', async () => { 101 | const verifyEmailIdentityMock = sinon.fake.resolves(true); 102 | AWS.mock('SES', 'verifyEmailIdentity', verifyEmailIdentityMock); 103 | 104 | const data = await onEventHandler(createEvent); 105 | 106 | sinon.assert.calledWith(verifyEmailIdentityMock, { 107 | EmailAddress: 'test@test.com', 108 | }); 109 | 110 | expect(data).toEqual({ 111 | PhysicalResourceId: 'validateEmail', 112 | }); 113 | }); 114 | 115 | test('on event does not call verifyEmailIdentity for Update requests', async () => { 116 | const verifyEmailIdentityMock = sinon.fake.resolves(true); 117 | AWS.mock('SES', 'verifyEmailIdentity', verifyEmailIdentityMock); 118 | 119 | await onEventHandler({ 120 | ...createEvent, 121 | RequestType: 'Update', 122 | }); 123 | sinon.assert.notCalled(verifyEmailIdentityMock); 124 | }); 125 | 126 | test('is complete will throw error without requestId', async () => { 127 | const getIdentityVerificationMock = sinon.fake.resolves(true); 128 | 129 | AWS.mock( 130 | 'SES', 131 | 'getIdentityVerificationAttributes', 132 | getIdentityVerificationMock, 133 | ); 134 | 135 | try { 136 | await isCompleteHandler({ 137 | ...isCompleteCreateEvent, 138 | PhysicalResourceId: undefined, 139 | }); 140 | sinon.assert.fail(); 141 | } catch (error) { 142 | sinon.assert.notCalled(getIdentityVerificationMock); 143 | expect((error as Error).message).toEqual('Missing PhysicalResourceId parameter.'); 144 | } 145 | }); 146 | 147 | test('is complete for create returns false when email is not verified yet', async () => { 148 | const getIdentityVerificationMock = sinon.fake.resolves({ 149 | VerificationAttributes: { 150 | 'test@test.com': { VerificationStatus: 'Progress' }, 151 | }, 152 | }); 153 | 154 | AWS.mock( 155 | 'SES', 156 | 'getIdentityVerificationAttributes', 157 | getIdentityVerificationMock, 158 | ); 159 | 160 | const data: any = await isCompleteHandler(isCompleteCreateEvent); 161 | 162 | expect(data.IsComplete).toBeFalsy; 163 | }); 164 | 165 | test('is complete for create returns true when email is verified', async () => { 166 | const getIdentityVerificationMock = sinon.fake.resolves({ 167 | VerificationAttributes: { 168 | 'test@test.com': { VerificationStatus: 'Success' }, 169 | }, 170 | }); 171 | 172 | AWS.mock( 173 | 'SES', 174 | 'getIdentityVerificationAttributes', 175 | getIdentityVerificationMock, 176 | ); 177 | 178 | const data: any = await isCompleteHandler(isCompleteCreateEvent); 179 | 180 | expect(data.IsComplete).toBeTruthy; 181 | }); 182 | 183 | 184 | test('on event delete no calls ses verifyEmailIdentity', async () => { 185 | const verifyEmailIdentityMock = sinon.fake.resolves(true); 186 | 187 | AWS.mock('SES', 'verifyEmailIdentity', verifyEmailIdentityMock); 188 | 189 | const data = await onEventHandler(deleteEvent); 190 | 191 | sinon.assert.notCalled(verifyEmailIdentityMock); 192 | 193 | expect(data).toEqual({ 194 | PhysicalResourceId: 'validateEmail', 195 | }); 196 | }); 197 | 198 | test('is complete for delete returns true when email is verified', async () => { 199 | 200 | const data: any = await isCompleteHandler(isCompleteDeleteEvent); 201 | 202 | expect(data.IsComplete).toBeTruthy; 203 | }); -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | --------------------------------------------------------------------------------